diff --git a/.DS_Store b/.DS_Store old mode 100644 new mode 100755 index f636302..b91b452 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.editorconfig b/.editorconfig new file mode 100755 index 0000000..d2d7f0f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +[*.{css,scss,less,js,json,ts,sass,html,hbs,mustache,phtml,html.twig,md,yml}] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +indent_size = 4 +trim_trailing_whitespace = false + +[site/templates/**.php] +indent_size = 2 + +[site/snippets/**.php] +indent_size = 2 + +[package.json,.{babelrc,editorconfig,eslintrc,lintstagedrc,stylelintrc}] +indent_style = space +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..f9c9e22 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# System files +# ------------ + +Icon +.DS_Store + +# Temporary files +# --------------- + +/media/* +!/media/index.html + +# Lock files +# --------------- +.lock + +# -------------SECURITY------------- +# NEVER publish these files via Git! +# -------------SECURITY------------- + +# Cache Files +# --------------- + +/site/cache/* +!/site/cache/index.html + +# Accounts +# --------------- + +/site/accounts/* +!/site/accounts/index.html + +# Sessions +# --------------- + +/site/sessions/* +!/site/sessions/index.html + +# License +# --------------- +/site/config/.license diff --git a/.htaccess b/.htaccess new file mode 100755 index 0000000..4d54dfe --- /dev/null +++ b/.htaccess @@ -0,0 +1,61 @@ +# Kirby .htaccess + +# rewrite rules + + +# enable awesome urls. i.e.: +# http://yourdomain.com/about-us/team +RewriteEngine on + +# make sure to set the RewriteBase correctly +# if you are running the site in a subfolder. +# Otherwise links or the entire site will break. +# +# If your homepage is http://yourdomain.com/mysite +# Set the RewriteBase to: +# +# RewriteBase /mysite + +# In some environments it's necessary to +# set the RewriteBase to: +# +# RewriteBase / + +# block files and folders beginning with a dot, such as .git +# except for the .well-known folder, which is used for Let's Encrypt and security.txt +RewriteRule (^|/)\.(?!well-known\/) index.php [L] + +# block text files in the content folder from being accessed directly +RewriteRule ^content/(.*)\.(txt|md|mdown)$ index.php [L] + +# block all files in the site folder from being accessed directly +# except for requests to plugin assets files +RewriteRule ^site/(.*) index.php [L] + +# Enable authentication header +SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 + +# block direct access to kirby and the panel sources +RewriteRule ^kirby/(.*) index.php [L] + +# make site links work +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^(.*) index.php [L] + + + +# compress text file responses + +AddOutputFilterByType DEFLATE text/plain +AddOutputFilterByType DEFLATE text/html +AddOutputFilterByType DEFLATE text/css +AddOutputFilterByType DEFLATE text/javascript +AddOutputFilterByType DEFLATE application/json +AddOutputFilterByType DEFLATE application/javascript +AddOutputFilterByType DEFLATE application/x-javascript + + +php_value upload_max_filesize 1G +php_value post_max_size 1G +php_value memory_limit 1G diff --git a/README.md b/README.md new file mode 100755 index 0000000..68a2d09 --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# Kirby Plainkit + +Kirby is a file-based CMS. +Easy to setup. Easy to use. Flexible as hell. + +## Trial + +You can try Kirby on your local machine or on a test +server as long as you need to make sure it is the right +tool for your next project. + +## Buy a license + +You can purchase your Kirby license at + + +A Kirby license is valid for a single domain. You can find +Kirby's license agreement here: + +## The Plainkit + +Kirby's Plainkit is the most minimal setup you can get started with. +It does not include any content, styles or other kinds of decoration, +so it's perfect to use this as a starting point for your own project. + +## The Panel + +You can find the login for Kirby's admin interface at +http://yourdomain.com/panel. You will be guided through the signup +process for your first user, when you visit the panel +for the first time. + +## Installation + +Kirby does not require a database, which makes it very easy to +install. Just copy Kirby's files to your server and visit the +URL for your website in the browser. + +**Please check if the invisible .htaccess file has been +copied to your server correctly** + +### Requirements + +Kirby runs on PHP 7.1+, Apache or Nginx. + +### Download + +You can download the latest version of the Plainkit +from https://github.com/getkirby/plainkit/archive/master.zip + +### With Git + +If you are familiar with Git, you can clone Kirby's +Plainkit repository from Github. + + git clone https://github.com/getkirby/plainkit.git + +## Documentation + + + +## Issues + +If you have a Github account, please report issues +directly on Github: + +Otherwise you can use Kirby's forum: https://forum.getkirby.com +or send us an email: + +## Ideas & Feature Requests + +If you have ideas for new features, please submit a ticket in our ideas repository: + + +## Support + + + +## Copyright + +© 2009-2019 Bastian Allgeier (Bastian Allgeier GmbH) + diff --git a/fonts/CenturySchL-Bold.woff b/assets/fonts/CenturySchL-Bold.woff old mode 100644 new mode 100755 similarity index 100% rename from fonts/CenturySchL-Bold.woff rename to assets/fonts/CenturySchL-Bold.woff diff --git a/fonts/CenturySchL-Bold.woff2 b/assets/fonts/CenturySchL-Bold.woff2 old mode 100644 new mode 100755 similarity index 100% rename from fonts/CenturySchL-Bold.woff2 rename to assets/fonts/CenturySchL-Bold.woff2 diff --git a/fonts/CenturySchL-BoldItal.woff b/assets/fonts/CenturySchL-BoldItal.woff old mode 100644 new mode 100755 similarity index 100% rename from fonts/CenturySchL-BoldItal.woff rename to assets/fonts/CenturySchL-BoldItal.woff diff --git a/fonts/CenturySchL-BoldItal.woff2 b/assets/fonts/CenturySchL-BoldItal.woff2 old mode 100644 new mode 100755 similarity index 100% rename from fonts/CenturySchL-BoldItal.woff2 rename to assets/fonts/CenturySchL-BoldItal.woff2 diff --git a/fonts/CenturySchL-Ital.woff b/assets/fonts/CenturySchL-Ital.woff old mode 100644 new mode 100755 similarity index 100% rename from fonts/CenturySchL-Ital.woff rename to assets/fonts/CenturySchL-Ital.woff diff --git a/fonts/CenturySchL-Ital.woff2 b/assets/fonts/CenturySchL-Ital.woff2 old mode 100644 new mode 100755 similarity index 100% rename from fonts/CenturySchL-Ital.woff2 rename to assets/fonts/CenturySchL-Ital.woff2 diff --git a/fonts/CenturySchL-Roma.woff b/assets/fonts/CenturySchL-Roma.woff old mode 100644 new mode 100755 similarity index 100% rename from fonts/CenturySchL-Roma.woff rename to assets/fonts/CenturySchL-Roma.woff diff --git a/fonts/CenturySchL-Roma.woff2 b/assets/fonts/CenturySchL-Roma.woff2 old mode 100644 new mode 100755 similarity index 100% rename from fonts/CenturySchL-Roma.woff2 rename to assets/fonts/CenturySchL-Roma.woff2 diff --git a/fonts/CenturySchoolbookBT-Monospace.woff b/assets/fonts/CenturySchoolbookBT-Monospace.woff old mode 100644 new mode 100755 similarity index 100% rename from fonts/CenturySchoolbookBT-Monospace.woff rename to assets/fonts/CenturySchoolbookBT-Monospace.woff diff --git a/fonts/CenturySchoolbookBT-Monospace.woff2 b/assets/fonts/CenturySchoolbookBT-Monospace.woff2 old mode 100644 new mode 100755 similarity index 100% rename from fonts/CenturySchoolbookBT-Monospace.woff2 rename to assets/fonts/CenturySchoolbookBT-Monospace.woff2 diff --git a/fonts/stylesheet.css b/assets/fonts/stylesheet.css old mode 100644 new mode 100755 similarity index 100% rename from fonts/stylesheet.css rename to assets/fonts/stylesheet.css diff --git a/main.css b/assets/main.css old mode 100644 new mode 100755 similarity index 100% rename from main.css rename to assets/main.css diff --git a/composer.json b/composer.json new file mode 100755 index 0000000..bdcd265 --- /dev/null +++ b/composer.json @@ -0,0 +1,27 @@ +{ + "name": "getkirby/plainkit", + "description": "Kirby Plainkit", + "type": "project", + "keywords": ["kirby", "cms", "starterkit"], + "homepage": "https://getkirby.com", + "authors": [ + { + "name": "Bastian Allgeier", + "email": "bastian@getkirby.com", + "homepage": "https://getkirby.com" + } + ], + "support": { + "email": "support@getkirby.com", + "issues": "https://github.com/getkirby/starterkit/issues", + "forum": "https://forum.getkirby.com", + "source": "https://github.com/getkirby/starterkit" + }, + "require": { + "php": ">=7.1.0", + "getkirby/cms": "^3.0" + }, + "config": { + "optimize-autoloader": true + } +} diff --git a/content/error/error.txt b/content/error/error.txt new file mode 100755 index 0000000..b588b2a --- /dev/null +++ b/content/error/error.txt @@ -0,0 +1 @@ +Title: Error \ No newline at end of file diff --git a/content/home/home.txt b/content/home/home.txt new file mode 100755 index 0000000..02896ec --- /dev/null +++ b/content/home/home.txt @@ -0,0 +1 @@ +Title: Home \ No newline at end of file diff --git a/content/site.txt b/content/site.txt new file mode 100755 index 0000000..76234f7 --- /dev/null +++ b/content/site.txt @@ -0,0 +1 @@ +Title: Recipes for Food \ No newline at end of file diff --git a/fonts/.DS_Store b/fonts/.DS_Store deleted file mode 100644 index 5008ddf..0000000 Binary files a/fonts/.DS_Store and /dev/null differ diff --git a/images/postcards-hires.jpg b/images/postcards-hires.jpg deleted file mode 100644 index dddc9c5..0000000 Binary files a/images/postcards-hires.jpg and /dev/null differ diff --git a/images/postcards-hires2.jpg b/images/postcards-hires2.jpg deleted file mode 100644 index 43c8a7b..0000000 Binary files a/images/postcards-hires2.jpg and /dev/null differ diff --git a/images/postcards-hires3.jpg b/images/postcards-hires3.jpg deleted file mode 100644 index d9e42ec..0000000 Binary files a/images/postcards-hires3.jpg and /dev/null differ diff --git a/images/postcards-hires4.jpg b/images/postcards-hires4.jpg deleted file mode 100644 index c08ac8c..0000000 Binary files a/images/postcards-hires4.jpg and /dev/null differ diff --git a/images/postcards-hires5.jpg b/images/postcards-hires5.jpg deleted file mode 100644 index de6a367..0000000 Binary files a/images/postcards-hires5.jpg and /dev/null differ diff --git a/images/postcards-hires6.jpg b/images/postcards-hires6.jpg deleted file mode 100644 index e034d2e..0000000 Binary files a/images/postcards-hires6.jpg and /dev/null differ diff --git a/images/postcards-hires7.jpg b/images/postcards-hires7.jpg deleted file mode 100644 index 2991187..0000000 Binary files a/images/postcards-hires7.jpg and /dev/null differ diff --git a/images/postcards-hires8.jpg b/images/postcards-hires8.jpg deleted file mode 100644 index fed76aa..0000000 Binary files a/images/postcards-hires8.jpg and /dev/null differ diff --git a/images/postcards-hires9.jpg b/images/postcards-hires9.jpg deleted file mode 100644 index 469109c..0000000 Binary files a/images/postcards-hires9.jpg and /dev/null differ diff --git a/index.html b/index.html deleted file mode 100644 index f398b17..0000000 --- a/index.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - Recipes for Food - - - - - - - -
-
-
- - - - - - - - - -
-
-
-

Recipes for Food

-

- Edited by Nazli Ercan and Eric Li
- We are looking for recipes! Please submit one via email. -

-
-
Coming Very Soon!
-
-
-
- - - - - - - diff --git a/index.php b/index.php new file mode 100755 index 0000000..87ed01d --- /dev/null +++ b/index.php @@ -0,0 +1,5 @@ +render(); diff --git a/kirby/.editorconfig b/kirby/.editorconfig new file mode 100755 index 0000000..adbc151 --- /dev/null +++ b/kirby/.editorconfig @@ -0,0 +1,15 @@ +# This file is for unifying the coding style for different editors and IDEs +# editorconfig.org + +# PHP PSR-2 Coding Standards +# http://www.php-fig.org/psr/psr-2/ + +root = true + +[*.php] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 \ No newline at end of file diff --git a/kirby/.php_cs b/kirby/.php_cs new file mode 100755 index 0000000..07cd80f --- /dev/null +++ b/kirby/.php_cs @@ -0,0 +1,58 @@ +exclude('dependencies') + ->in(__DIR__); + +return PhpCsFixer\Config::create() + ->setRules([ + '@PSR1' => true, + '@PSR2' => true, + 'align_multiline_comment' => ['comment_type' => 'phpdocs_like'], + 'array_indentation' => true, + 'array_syntax' => ['syntax' => 'short'], + 'cast_spaces' => ['space' => 'none'], + // 'class_keyword_remove' => true, // replaces static::class with 'static' (won't work) + 'combine_consecutive_issets' => true, + 'combine_consecutive_unsets' => true, + 'combine_nested_dirname' => true, + 'concat_space' => ['spacing' => 'one'], + 'declare_equal_normalize' => ['space' => 'single'], + 'dir_constant' => true, + 'function_typehint_space' => true, + 'include' => true, + 'logical_operators' => true, + 'lowercase_cast' => true, + 'lowercase_static_reference' => true, + 'magic_constant_casing' => true, + 'magic_method_casing' => true, + 'method_chaining_indentation' => true, + 'modernize_types_casting' => true, + 'multiline_comment_opening_closing' => true, + 'native_function_casing' => true, + 'native_function_type_declaration_casing' => true, + 'new_with_braces' => true, + 'no_blank_lines_after_class_opening' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_empty_comment' => true, + 'no_empty_phpdoc' => true, + 'no_empty_statement' => true, + 'no_leading_namespace_whitespace' => true, + 'no_mixed_echo_print' => ['use' => 'echo'], + 'no_unneeded_control_parentheses' => true, + 'no_unused_imports' => true, + 'no_useless_return' => true, + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + // 'phpdoc_add_missing_param_annotation' => ['only_untyped' => false], // adds params in the wrong order + 'phpdoc_align' => ['align' => 'left'], + 'phpdoc_indent' => true, + 'phpdoc_scalar' => true, + 'phpdoc_trim' => true, + 'short_scalar_cast' => true, + 'single_line_comment_style' => true, + 'single_quote' => true, + 'ternary_to_null_coalescing' => true, + 'whitespace_after_comma_in_array' => true + ]) + ->setRiskyAllowed(true) + ->setFinder($finder); diff --git a/kirby/SECURITY.md b/kirby/SECURITY.md new file mode 100755 index 0000000..d3ea685 --- /dev/null +++ b/kirby/SECURITY.md @@ -0,0 +1,16 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 3.2.3+ | :white_check_mark: | +| 2.5.12+ | :white_check_mark: | + +## Security Guide + +Please follow our security guide to keep your Kirby installation safe: https://getkirby.com/docs/guide/security#reporting-security-issues + +## Reporting a Vulnerability + +If you have spotted a vulnerability in Kirby's core or the Panel, please make sure to let us know immediately. We take any report very seriously. You can always write us directly at support@getkirby.com. Please do not write to us publicly, e.g. in the forum. diff --git a/kirby/bootstrap.php b/kirby/bootstrap.php new file mode 100755 index 0000000..0f593d0 --- /dev/null +++ b/kirby/bootstrap.php @@ -0,0 +1,32 @@ +=') === false) { + 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; +} elseif (is_file($autoloader = __DIR__ . '/vendor/autoload.php')) { + + /** + * 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 + */ +} diff --git a/kirby/composer.json b/kirby/composer.json new file mode 100755 index 0000000..02941d1 --- /dev/null +++ b/kirby/composer.json @@ -0,0 +1,52 @@ +{ + "name": "getkirby/cms", + "description": "The Kirby 3 core", + "version": "3.3.3", + "license": "proprietary", + "keywords": ["kirby", "cms", "core"], + "homepage": "https://getkirby.com", + "type": "kirby-cms", + "authors": [ + { + "name": "Kirby Team", + "email": "support@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.1.0", + "ext-mbstring": "*", + "ext-ctype": "*", + "getkirby/composer-installer": "^1.0", + "mustangostang/spyc": "0.6.2", + "michelf/php-smartypants": "1.8.1", + "claviska/simpleimage": "3.3.3", + "phpmailer/phpmailer": "6.0.7", + "filp/whoops": "2.3.1", + "true/punycode": "2.1.1", + "laminas/laminas-escaper": "2.6.0" + }, + "autoload": { + "files": ["config/setup.php"], + "classmap": ["dependencies/"], + "psr-4": { + "Kirby\\": "src/" + } + }, + "scripts": { + "analyze": "phpstan analyse", + "test": "phpunit --stderr --coverage-html=tests/coverage", + "zip": "composer archive --format=zip --file=dist", + "build": "./scripts/build", + "fix": "php-cs-fixer fix --config .php_cs" + }, + "config": { + "optimize-autoloader": true + } +} diff --git a/kirby/composer.lock b/kirby/composer.lock new file mode 100755 index 0000000..b209f45 --- /dev/null +++ b/kirby/composer.lock @@ -0,0 +1,633 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "d555f3a70378b49cf4e1eb249f8aeb85", + "packages": [ + { + "name": "claviska/simpleimage", + "version": "3.3.3", + "source": { + "type": "git", + "url": "https://github.com/claviska/SimpleImage.git", + "reference": "31ba5b8358e1663a2813e2ada7242fa8d97a96dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/claviska/SimpleImage/zipball/31ba5b8358e1663a2813e2ada7242fa8d97a96dc", + "reference": "31ba5b8358e1663a2813e2ada7242fa8d97a96dc", + "shasum": "" + }, + "require": { + "ext-gd": "*", + "league/color-extractor": "0.3.*", + "php": ">=5.6.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "claviska": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Cory LaViska", + "homepage": "http://www.abeautifulsite.net/", + "role": "Developer" + } + ], + "description": "A PHP class that makes working with images as simple as possible.", + "time": "2017-09-12T09:03:56+00:00" + }, + { + "name": "filp/whoops", + "version": "2.3.1", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "bc0fd11bc455cc20ee4b5edabc63ebbf859324c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/bc0fd11bc455cc20ee4b5edabc63ebbf859324c7", + "reference": "bc0fd11bc455cc20ee4b5edabc63ebbf859324c7", + "shasum": "" + }, + "require": { + "php": "^5.5.9 || ^7.0", + "psr/log": "^1.0.1" + }, + "require-dev": { + "mockery/mockery": "^0.9 || ^1.0", + "phpunit/phpunit": "^4.8.35 || ^5.7", + "symfony/var-dumper": "^2.6 || ^3.0 || ^4.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "time": "2018-10-23T09:00:00+00:00" + }, + { + "name": "getkirby/composer-installer", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/getkirby/composer-installer.git", + "reference": "2d6b8f5601a31caeeea45623e1643fbb437eb94e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getkirby/composer-installer/zipball/2d6b8f5601a31caeeea45623e1643fbb437eb94e", + "reference": "2d6b8f5601a31caeeea45623e1643fbb437eb94e", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0" + }, + "require-dev": { + "composer/composer": "^1.8", + "phpunit/phpunit": "^7.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Kirby\\ComposerInstaller\\Plugin" + }, + "autoload": { + "psr-4": { + "Kirby\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Kirby's custom Composer installer for the Kirby CMS and for Kirby plugins", + "homepage": "https://getkirby.com", + "time": "2019-02-11T20:27:36+00:00" + }, + { + "name": "laminas/laminas-escaper", + "version": "2.6.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-escaper.git", + "reference": "72d3a14647f6234cdd9386d6de821a98e539af91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/72d3a14647f6234cdd9386d6de821a98e539af91", + "reference": "72d3a14647f6234cdd9386d6de821a98e539af91", + "shasum": "" + }, + "require": { + "laminas/laminas-zendframework-bridge": "^1.0", + "php": "^5.6 || ^7.0" + }, + "replace": { + "zendframework/zend-escaper": "self.version" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~1.0.0", + "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6.x-dev", + "dev-develop": "2.7.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laminas\\Escaper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs", + "homepage": "https://laminas.dev", + "keywords": [ + "escaper", + "laminas" + ], + "time": "2019-12-31T16:43:29+00:00" + }, + { + "name": "laminas/laminas-zendframework-bridge", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-zendframework-bridge.git", + "reference": "0fb9675b84a1666ab45182b6c5b29956921e818d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/0fb9675b84a1666ab45182b6c5b29956921e818d", + "reference": "0fb9675b84a1666ab45182b6c5b29956921e818d", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev", + "dev-develop": "1.1.x-dev" + }, + "laminas": { + "module": "Laminas\\ZendFrameworkBridge" + } + }, + "autoload": { + "files": [ + "src/autoload.php" + ], + "psr-4": { + "Laminas\\ZendFrameworkBridge\\": "src//" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Alias legacy ZF class names to Laminas Project equivalents.", + "keywords": [ + "ZendFramework", + "autoloading", + "laminas", + "zf" + ], + "time": "2020-01-07T22:58:31+00:00" + }, + { + "name": "league/color-extractor", + "version": "0.3.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/color-extractor.git", + "reference": "837086ec60f50c84c611c613963e4ad2e2aec806" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/color-extractor/zipball/837086ec60f50c84c611c613963e4ad2e2aec806", + "reference": "837086ec60f50c84c611c613963e4ad2e2aec806", + "shasum": "" + }, + "require": { + "ext-gd": "*", + "php": ">=5.4.0" + }, + "replace": { + "matthecat/colorextractor": "*" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2", + "phpunit/phpunit": "~5" + }, + "type": "library", + "autoload": { + "psr-4": { + "": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathieu Lechat", + "email": "math.lechat@gmail.com", + "homepage": "http://matthecat.com", + "role": "Developer" + } + ], + "description": "Extract colors from an image as a human would do.", + "homepage": "https://github.com/thephpleague/color-extractor", + "keywords": [ + "color", + "extract", + "human", + "image", + "palette" + ], + "time": "2016-12-15T09:30:02+00:00" + }, + { + "name": "michelf/php-smartypants", + "version": "1.8.1", + "source": { + "type": "git", + "url": "https://github.com/michelf/php-smartypants.git", + "reference": "47d17c90a4dfd0ccf1f87e25c65e6c8012415aad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/michelf/php-smartypants/zipball/47d17c90a4dfd0ccf1f87e25c65e6c8012415aad", + "reference": "47d17c90a4dfd0ccf1f87e25c65e6c8012415aad", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Michelf": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Michel Fortin", + "email": "michel.fortin@michelf.ca", + "homepage": "https://michelf.ca/", + "role": "Developer" + }, + { + "name": "John Gruber", + "homepage": "https://daringfireball.net/" + } + ], + "description": "PHP SmartyPants", + "homepage": "https://michelf.ca/projects/php-smartypants/", + "keywords": [ + "dashes", + "quotes", + "spaces", + "typographer", + "typography" + ], + "time": "2016-12-13T01:01:17+00:00" + }, + { + "name": "mustangostang/spyc", + "version": "0.6.2", + "source": { + "type": "git", + "url": "https://github.com/mustangostang/spyc.git", + "reference": "23c35ae854d835f2d7bcc3e3ad743d7e57a8c14d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mustangostang/spyc/zipball/23c35ae854d835f2d7bcc3e3ad743d7e57a8c14d", + "reference": "23c35ae854d835f2d7bcc3e3ad743d7e57a8c14d", + "shasum": "" + }, + "require": { + "php": ">=5.3.1" + }, + "require-dev": { + "phpunit/phpunit": "4.3.*@dev" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.5.x-dev" + } + }, + "autoload": { + "files": [ + "Spyc.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "mustangostang", + "email": "vlad.andersen@gmail.com" + } + ], + "description": "A simple YAML loader/dumper class for PHP", + "homepage": "https://github.com/mustangostang/spyc/", + "keywords": [ + "spyc", + "yaml", + "yml" + ], + "time": "2017-02-24T16:06:33+00:00" + }, + { + "name": "phpmailer/phpmailer", + "version": "v6.0.7", + "source": { + "type": "git", + "url": "https://github.com/PHPMailer/PHPMailer.git", + "reference": "0c41a36d4508d470e376498c1c0c527aa36a2d59" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/0c41a36d4508d470e376498c1c0c527aa36a2d59", + "reference": "0c41a36d4508d470e376498c1c0c527aa36a2d59", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-filter": "*", + "php": ">=5.5.0" + }, + "require-dev": { + "doctrine/annotations": "1.2.*", + "friendsofphp/php-cs-fixer": "^2.2", + "phpdocumentor/phpdocumentor": "2.*", + "phpunit/phpunit": "^4.8 || ^5.7", + "zendframework/zend-eventmanager": "3.0.*", + "zendframework/zend-i18n": "2.7.3", + "zendframework/zend-serializer": "2.7.*" + }, + "suggest": { + "ext-mbstring": "Needed to send email in multibyte encoding charset", + "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", + "league/oauth2-google": "Needed for Google XOAUTH2 authentication", + "psr/log": "For optional PSR-3 debug logging", + "stevenmaguire/oauth2-microsoft": "Needed for Microsoft XOAUTH2 authentication", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPMailer\\PHPMailer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1" + ], + "authors": [ + { + "name": "Jim Jagielski", + "email": "jimjag@gmail.com" + }, + { + "name": "Marcus Bointon", + "email": "phpmailer@synchromedia.co.uk" + }, + { + "name": "Andy Prevost", + "email": "codeworxtech@users.sourceforge.net" + }, + { + "name": "Brent R. Matzelle" + } + ], + "description": "PHPMailer is a full-featured email creation and transfer class for PHP", + "time": "2019-02-01T15:04:28+00:00" + }, + { + "name": "psr/log", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/446d54b4cb6bf489fc9d75f55843658e6f25d801", + "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2019-11-01T11:05:21+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.13.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "7b4aab9743c30be783b73de055d24a39cf4b954f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7b4aab9743c30be783b73de055d24a39cf4b954f", + "reference": "7b4aab9743c30be783b73de055d24a39cf4b954f", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.13-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2019-11-27T14:18:11+00:00" + }, + { + "name": "true/punycode", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/true/php-punycode.git", + "reference": "a4d0c11a36dd7f4e7cd7096076cab6d3378a071e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/true/php-punycode/zipball/a4d0c11a36dd7f4e7cd7096076cab6d3378a071e", + "reference": "a4d0c11a36dd7f4e7cd7096076cab6d3378a071e", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.7", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "TrueBV\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Renan Gonçalves", + "email": "renan.saddam@gmail.com" + } + ], + "description": "A Bootstring encoding of Unicode for Internationalized Domain Names in Applications (IDNA)", + "homepage": "https://github.com/true/php-punycode", + "keywords": [ + "idna", + "punycode" + ], + "time": "2016-11-16T10:37:54+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=7.1.0", + "ext-mbstring": "*", + "ext-ctype": "*" + }, + "platform-dev": [] +} diff --git a/kirby/config/aliases.php b/kirby/config/aliases.php new file mode 100755 index 0000000..fc354a5 --- /dev/null +++ b/kirby/config/aliases.php @@ -0,0 +1,60 @@ + 'Kirby\Cms\Asset', + 'collection' => 'Kirby\Cms\Collection', + 'dir' => 'Kirby\Cms\Dir', + 'field' => 'Kirby\Cms\Field', + 'file' => 'Kirby\Cms\File', + 'files' => 'Kirby\Cms\Files', + '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', + '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 classes + 'database' => 'Kirby\Database\Database', + 'db' => 'Kirby\Database\Db', + + // exceptions + 'errorpageexception' => 'Kirby\Exception\ErrorPageException', + + // http classes + 'cookie' => 'Kirby\Http\Cookie', + 'header' => 'Kirby\Http\Header', + 'remote' => 'Kirby\Http\Remote', + 'server' => 'Kirby\Http\Server', + + // image classes + 'dimensions' => 'Kirby\Image\Dimensions', + + // toolkit classes + 'a' => 'Kirby\Toolkit\A', + 'c' => 'Kirby\Toolkit\Config', + 'config' => 'Kirby\Toolkit\Config', + 'escape' => 'Kirby\Toolkit\Escape', + 'f' => 'Kirby\Toolkit\F', + 'i18n' => 'Kirby\Toolkit\I18n', + 'mime' => 'Kirby\Toolkit\Mime', + 'obj' => 'Kirby\Toolkit\Obj', + 'str' => 'Kirby\Toolkit\Str', + 'tpl' => 'Kirby\Toolkit\Tpl', + 'v' => 'Kirby\Toolkit\V', + 'xml' => 'Kirby\Toolkit\Xml' +]; diff --git a/kirby/config/api/authentication.php b/kirby/config/api/authentication.php new file mode 100755 index 0000000..509a346 --- /dev/null +++ b/kirby/config/api/authentication.php @@ -0,0 +1,23 @@ +kirby()->auth(); + + // csrf token check + if ($auth->type() === 'session' && $auth->csrf() === false) { + throw new PermissionException('Unauthenticated'); + } + + // get user from session or basic auth + if ($user = $auth->user()) { + if ($user->role()->permissions()->for('access', 'panel') === false) { + throw new PermissionException(['key' => 'access.panel']); + } + + return $user; + } + + throw new PermissionException('Unauthenticated'); +}; diff --git a/kirby/config/api/collections.php b/kirby/config/api/collections.php new file mode 100755 index 0000000..bd817e9 --- /dev/null +++ b/kirby/config/api/collections.php @@ -0,0 +1,72 @@ + [ + 'model' => 'page', + 'type' => 'Kirby\Cms\Pages', + 'view' => 'compact' + ], + + /** + * Files + */ + 'files' => [ + 'model' => 'file', + 'type' => 'Kirby\Cms\Files' + ], + + /** + * Languages + */ + 'languages' => [ + 'model' => 'language', + 'type' => 'Kirby\Cms\Languages' + ], + + /** + * Pages + */ + 'pages' => [ + 'model' => 'page', + 'type' => 'Kirby\Cms\Pages', + 'view' => 'compact' + ], + + /** + * Roles + */ + 'roles' => [ + 'model' => 'role', + 'type' => 'Kirby\Cms\Roles', + 'view' => 'compact' + ], + + /** + * Translations + */ + 'translations' => [ + 'model' => 'translation', + 'type' => 'Kirby\Cms\Translations', + 'view' => 'compact' + ], + + /** + * Users + */ + 'users' => [ + 'default' => function () { + return $this->users(); + }, + 'model' => 'user', + 'type' => 'Kirby\Cms\Users', + 'view' => 'compact' + ] + +]; diff --git a/kirby/config/api/models.php b/kirby/config/api/models.php new file mode 100755 index 0000000..51d19fb --- /dev/null +++ b/kirby/config/api/models.php @@ -0,0 +1,20 @@ + 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 new file mode 100755 index 0000000..bf5e573 --- /dev/null +++ b/kirby/config/api/models/File.php @@ -0,0 +1,166 @@ + [ + 'blueprint' => function (File $file) { + return $file->blueprint(); + }, + 'content' => function (File $file) { + return Form::for($file)->values(); + }, + 'dimensions' => function (File $file) { + return $file->dimensions()->toArray(); + }, + 'dragText' => function (File $file) { + return $file->dragText(); + }, + 'exists' => function (File $file) { + return $file->exists(); + }, + 'extension' => function (File $file) { + return $file->extension(); + }, + 'filename' => function (File $file) { + return $file->filename(); + }, + 'id' => function (File $file) { + return $file->id(); + }, + 'link' => function (File $file) { + return $file->panelUrl(true); + }, + 'mime' => function (File $file) { + return $file->mime(); + }, + 'modified' => function (File $file) { + return $file->modified('c'); + }, + 'name' => function (File $file) { + return $file->name(); + }, + 'next' => function (File $file) { + return $file->next(); + }, + 'nextWithTemplate' => function (File $file) { + $files = $file->templateSiblings()->sortBy('sort', 'asc', 'filename', 'asc'); + $index = $files->indexOf($file); + + return $files->nth($index + 1); + }, + 'niceSize' => function (File $file) { + return $file->niceSize(); + }, + 'options' => function (File $file) { + return $file->panelOptions(); + }, + 'panelIcon' => function (File $file) { + return $file->panelIcon(); + }, + 'panelImage' => function (File $file) { + return $file->panelImage(); + }, + 'panelUrl' => function (File $file) { + return $file->panelUrl(true); + }, + 'prev' => function (File $file) { + return $file->prev(); + }, + 'prevWithTemplate' => function (File $file) { + $files = $file->templateSiblings()->sortBy('sort', 'asc', 'filename', 'asc'); + $index = $files->indexOf($file); + + return $files->nth($index - 1); + }, + 'parent' => function (File $file) { + return $file->parent(); + }, + 'parents' => function (File $file) { + return $file->parents()->flip(); + }, + 'size' => function (File $file) { + return $file->size(); + }, + 'template' => function (File $file) { + return $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' => function (File $file) { + return $file->type(); + }, + 'url' => function (File $file) { + return $file->url(true); + }, + ], + '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 new file mode 100755 index 0000000..0ca7cc0 --- /dev/null +++ b/kirby/config/api/models/FileBlueprint.php @@ -0,0 +1,26 @@ + [ + 'name' => function (FileBlueprint $blueprint) { + return $blueprint->name(); + }, + 'options' => function (FileBlueprint $blueprint) { + return $blueprint->options(); + }, + 'tabs' => function (FileBlueprint $blueprint) { + return $blueprint->tabs(); + }, + 'title' => function (FileBlueprint $blueprint) { + return $blueprint->title(); + }, + ], + 'type' => 'Kirby\Cms\FileBlueprint', + 'views' => [ + ], +]; diff --git a/kirby/config/api/models/FileVersion.php b/kirby/config/api/models/FileVersion.php new file mode 100755 index 0000000..3d518b3 --- /dev/null +++ b/kirby/config/api/models/FileVersion.php @@ -0,0 +1,83 @@ + [ + 'dimensions' => function (FileVersion $file) { + return $file->dimensions()->toArray(); + }, + 'exists' => function (FileVersion $file) { + return $file->exists(); + }, + 'extension' => function (FileVersion $file) { + return $file->extension(); + }, + 'filename' => function (FileVersion $file) { + return $file->filename(); + }, + 'id' => function (FileVersion $file) { + return $file->id(); + }, + 'mime' => function (FileVersion $file) { + return $file->mime(); + }, + 'modified' => function (FileVersion $file) { + return $file->modified('c'); + }, + 'name' => function (FileVersion $file) { + return $file->name(); + }, + 'niceSize' => function (FileVersion $file) { + return $file->niceSize(); + }, + 'size' => function (FileVersion $file) { + return $file->size(); + }, + 'type' => function (FileVersion $file) { + return $file->type(); + }, + 'url' => function (FileVersion $file) { + return $file->url(true); + }, + ], + '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 new file mode 100755 index 0000000..9350a89 --- /dev/null +++ b/kirby/config/api/models/Language.php @@ -0,0 +1,44 @@ + [ + 'code' => function (Language $language) { + return $language->code(); + }, + 'default' => function (Language $language) { + return $language->isDefault(); + }, + 'direction' => function (Language $language) { + return $language->direction(); + }, + 'locale' => function (Language $language) { + return $language->locale(); + }, + 'name' => function (Language $language) { + return $language->name(); + }, + 'rules' => function (Language $language) { + return $language->rules(); + }, + 'url' => function (Language $language) { + return $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 new file mode 100755 index 0000000..1f8467b --- /dev/null +++ b/kirby/config/api/models/Page.php @@ -0,0 +1,157 @@ + [ + 'blueprint' => function (Page $page) { + return $page->blueprint(); + }, + 'blueprints' => function (Page $page) { + return $page->blueprints(); + }, + 'children' => function (Page $page) { + return $page->children(); + }, + 'content' => function (Page $page) { + return Form::for($page)->values(); + }, + 'drafts' => function (Page $page) { + return $page->drafts(); + }, + 'errors' => function (Page $page) { + return $page->errors(); + }, + 'files' => function (Page $page) { + return $page->files()->sortBy('sort', 'asc', 'filename', 'asc'); + }, + 'hasChildren' => function (Page $page) { + return $page->hasChildren(); + }, + 'hasDrafts' => function (Page $page) { + return $page->hasDrafts(); + }, + 'hasFiles' => function (Page $page) { + return $page->hasFiles(); + }, + 'id' => function (Page $page) { + return $page->id(); + }, + 'isSortable' => function (Page $page) { + return $page->isSortable(); + }, + 'next' => function (Page $page) { + return $page + ->nextAll() + ->filterBy('intendedTemplate', $page->intendedTemplate()) + ->filterBy('status', $page->status()) + ->filterBy('isReadable', true) + ->first(); + }, + 'num' => function (Page $page) { + return $page->num(); + }, + 'options' => function (Page $page) { + return $page->panelOptions(['preview']); + }, + 'panelIcon' => function (Page $page) { + return $page->panelIcon(); + }, + 'panelImage' => function (Page $page) { + return $page->panelImage(); + }, + 'parent' => function (Page $page) { + return $page->parent(); + }, + 'parents' => function (Page $page) { + return $page->parents()->flip(); + }, + 'prev' => function (Page $page) { + return $page + ->prevAll() + ->filterBy('intendedTemplate', $page->intendedTemplate()) + ->filterBy('status', $page->status()) + ->filterBy('isReadable', true) + ->last(); + }, + 'previewUrl' => function (Page $page) { + return $page->previewUrl(); + }, + 'siblings' => function (Page $page) { + if ($page->isDraft() === true) { + return $page->parentModel()->children()->not($page); + } else { + return $page->siblings(); + } + }, + 'slug' => function (Page $page) { + return $page->slug(); + }, + 'status' => function (Page $page) { + return $page->status(); + }, + 'template' => function (Page $page) { + return $page->intendedTemplate()->name(); + }, + 'title' => function (Page $page) { + return $page->title()->value(); + }, + 'url' => function (Page $page) { + return $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 new file mode 100755 index 0000000..fd4cdef --- /dev/null +++ b/kirby/config/api/models/PageBlueprint.php @@ -0,0 +1,35 @@ + [ + 'name' => function (PageBlueprint $blueprint) { + return $blueprint->name(); + }, + 'num' => function (PageBlueprint $blueprint) { + return $blueprint->num(); + }, + 'options' => function (PageBlueprint $blueprint) { + return $blueprint->options(); + }, + 'preview' => function (PageBlueprint $blueprint) { + return $blueprint->preview(); + }, + 'status' => function (PageBlueprint $blueprint) { + return $blueprint->status(); + }, + 'tabs' => function (PageBlueprint $blueprint) { + return $blueprint->tabs(); + }, + 'title' => function (PageBlueprint $blueprint) { + return $blueprint->title(); + }, + ], + 'type' => 'Kirby\Cms\PageBlueprint', + 'views' => [ + ], +]; diff --git a/kirby/config/api/models/Role.php b/kirby/config/api/models/Role.php new file mode 100755 index 0000000..99aed16 --- /dev/null +++ b/kirby/config/api/models/Role.php @@ -0,0 +1,31 @@ + [ + 'description' => function (Role $role) { + return $role->description(); + }, + 'name' => function (Role $role) { + return $role->name(); + }, + 'permissions' => function (Role $role) { + return $role->permissions()->toArray(); + }, + 'title' => function (Role $role) { + return $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 new file mode 100755 index 0000000..ca5dfe0 --- /dev/null +++ b/kirby/config/api/models/Site.php @@ -0,0 +1,72 @@ + function () { + return $this->site(); + }, + 'fields' => [ + 'blueprint' => function (Site $site) { + return $site->blueprint(); + }, + 'children' => function (Site $site) { + return $site->children(); + }, + 'content' => function (Site $site) { + return Form::for($site)->values(); + }, + 'drafts' => function (Site $site) { + return $site->drafts(); + }, + 'files' => function (Site $site) { + return $site->files()->sortBy('sort', 'asc', 'filename', 'asc'); + }, + 'options' => function (Site $site) { + return $site->permissions()->toArray(); + }, + 'previewUrl' => function (Site $site) { + return $site->previewUrl(); + }, + 'title' => function (Site $site) { + return $site->title()->value(); + }, + 'url' => function (Site $site) { + return $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 new file mode 100755 index 0000000..a3b0943 --- /dev/null +++ b/kirby/config/api/models/SiteBlueprint.php @@ -0,0 +1,26 @@ + [ + 'name' => function (SiteBlueprint $blueprint) { + return $blueprint->name(); + }, + 'options' => function (SiteBlueprint $blueprint) { + return $blueprint->options(); + }, + 'tabs' => function (SiteBlueprint $blueprint) { + return $blueprint->tabs(); + }, + 'title' => function (SiteBlueprint $blueprint) { + return $blueprint->title(); + }, + ], + 'type' => 'Kirby\Cms\SiteBlueprint', + 'views' => [ + ], +]; diff --git a/kirby/config/api/models/System.php b/kirby/config/api/models/System.php new file mode 100755 index 0000000..72fbd6e --- /dev/null +++ b/kirby/config/api/models/System.php @@ -0,0 +1,113 @@ + [ + 'ascii' => function () { + return Str::$ascii; + }, + 'defaultLanguage' => function () { + return $this->kirby()->option('panel.language', 'en'); + }, + 'isOk' => function (System $system) { + return $system->isOk(); + }, + 'isInstallable' => function (System $system) { + return $system->isInstallable(); + }, + 'isInstalled' => function (System $system) { + return $system->isInstalled(); + }, + 'isLocal' => function (System $system) { + return $system->isLocal(); + }, + 'multilang' => function () { + return $this->kirby()->option('languages', false) !== false; + }, + 'languages' => function () { + return $this->kirby()->languages(); + }, + 'license' => function (System $system) { + return $system->license(); + }, + 'requirements' => function (System $system) { + return $system->toArray(); + }, + 'site' => function () { + try { + return $this->site()->blueprint()->title(); + } catch (Throwable $e) { + return $this->site()->title()->value(); + } + }, + 'slugs' => function () { + return Str::$language; + }, + 'title' => function () { + return $this->site()->title()->value(); + }, + 'translation' => function () { + if ($user = $this->user()) { + $translationCode = $user->language(); + } else { + $translationCode = $this->kirby()->option('panel.language', 'en'); + } + + if ($translation = $this->kirby()->translation($translationCode)) { + return $translation; + } else { + return $this->kirby()->translation('en'); + } + }, + 'kirbytext' => function () { + return $this->kirby()->option('panel.kirbytext') ?? true; + }, + 'user' => function () { + return $this->user(); + }, + 'version' => function () { + return $this->kirby()->version(); + } + ], + 'type' => 'Kirby\Cms\System', + 'views' => [ + 'login' => [ + 'isOk', + 'isInstallable', + 'isInstalled', + 'title', + 'translation' + ], + 'troubleshooting' => [ + 'isOk', + 'isInstallable', + 'isInstalled', + 'title', + 'translation', + 'requirements' + ], + 'panel' => [ + 'ascii', + 'defaultLanguage', + 'isOk', + 'isInstalled', + 'isLocal', + 'kirbytext', + 'languages', + 'license', + '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 new file mode 100755 index 0000000..13be9a0 --- /dev/null +++ b/kirby/config/api/models/Translation.php @@ -0,0 +1,34 @@ + [ + 'author' => function (Translation $translation) { + return $translation->author(); + }, + 'data' => function (Translation $translation) { + return $translation->dataWithFallback(); + }, + 'direction' => function (Translation $translation) { + return $translation->direction(); + }, + 'id' => function (Translation $translation) { + return $translation->id(); + }, + 'name' => function (Translation $translation) { + return $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 new file mode 100755 index 0000000..f0c6b19 --- /dev/null +++ b/kirby/config/api/models/User.php @@ -0,0 +1,105 @@ + function () { + return $this->user(); + }, + 'fields' => [ + 'avatar' => function (User $user) { + return $user->avatar() ? $user->avatar()->crop(512) : null; + }, + 'blueprint' => function (User $user) { + return $user->blueprint(); + }, + 'content' => function (User $user) { + return Form::for($user)->values(); + }, + 'email' => function (User $user) { + return $user->email(); + }, + 'files' => function (User $user) { + return $user->files()->sortBy('sort', 'asc', 'filename', 'asc'); + }, + 'id' => function (User $user) { + return $user->id(); + }, + 'language' => function (User $user) { + return $user->language(); + }, + 'name' => function (User $user) { + return $user->name()->value(); + }, + 'next' => function (User $user) { + return $user->next(); + }, + 'options' => function (User $user) { + return $user->panelOptions(); + }, + 'permissions' => function (User $user) { + return $user->role()->permissions()->toArray(); + }, + 'prev' => function (User $user) { + return $user->prev(); + }, + 'role' => function (User $user) { + return $user->role(); + }, + 'username' => function (User $user) { + return $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 new file mode 100755 index 0000000..fdf63eb --- /dev/null +++ b/kirby/config/api/models/UserBlueprint.php @@ -0,0 +1,26 @@ + [ + 'name' => function (UserBlueprint $blueprint) { + return $blueprint->name(); + }, + 'options' => function (UserBlueprint $blueprint) { + return $blueprint->options(); + }, + 'tabs' => function (UserBlueprint $blueprint) { + return $blueprint->tabs(); + }, + 'title' => function (UserBlueprint $blueprint) { + return $blueprint->title(); + }, + ], + 'type' => 'Kirby\Cms\UserBlueprint', + 'views' => [ + ], +]; diff --git a/kirby/config/api/routes.php b/kirby/config/api/routes.php new file mode 100755 index 0000000..fd3449d --- /dev/null +++ b/kirby/config/api/routes.php @@ -0,0 +1,26 @@ +option('languages', false) !== false) { + $routes = array_merge($routes, include __DIR__ . '/routes/languages.php'); + } + + return $routes; +}; diff --git a/kirby/config/api/routes/auth.php b/kirby/config/api/routes/auth.php new file mode 100755 index 0000000..7e4dcb4 --- /dev/null +++ b/kirby/config/api/routes/auth.php @@ -0,0 +1,55 @@ + '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/login', + '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'); + } + + $email = $this->requestBody('email'); + $long = $this->requestBody('long'); + $password = $this->requestBody('password'); + + $user = $this->kirby()->auth()->login($email, $password, $long); + + return [ + 'code' => 200, + 'status' => 'ok', + 'user' => $this->resolve($user)->view('auth')->toArray() + ]; + } + ], + [ + '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 new file mode 100755 index 0000000..8c5414e --- /dev/null +++ b/kirby/config/api/routes/files.php @@ -0,0 +1,107 @@ + '(:all)/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' => '(:all)/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' => '(:all)/files', + 'method' => 'GET', + 'action' => function (string $path) { + return $this->parent($path)->files()->sortBy('sort', 'asc', 'filename', 'asc'); + } + ], + [ + 'pattern' => '(:all)/files', + 'method' => 'POST', + 'action' => function (string $path) { + return $this->upload(function ($source, $filename) use ($path) { + return $this->parent($path)->createFile([ + 'source' => $source, + 'template' => $this->requestBody('template'), + 'filename' => $filename + ]); + }); + } + ], + [ + 'pattern' => '(:all)/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' => '(:all)/files/sort', + 'method' => 'PATCH', + 'action' => function (string $path) { + return $this->parent($path)->files()->changeSort( + $this->requestBody('files'), + $this->requestBody('index') + ); + } + ], + [ + 'pattern' => '(:all)/files/(:any)', + 'method' => 'GET', + 'action' => function (string $path, string $filename) { + return $this->file($path, $filename); + } + ], + [ + 'pattern' => '(:all)/files/(:any)', + 'method' => 'PATCH', + 'action' => function (string $path, string $filename) { + return $this->file($path, $filename)->update($this->requestBody(), $this->language(), true); + } + ], + [ + 'pattern' => '(:all)/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' => '(:all)/files/(:any)', + 'method' => 'DELETE', + 'action' => function (string $path, string $filename) { + return $this->file($path, $filename)->delete(); + } + ], + [ + 'pattern' => '(:all)/files/(:any)/name', + 'method' => 'PATCH', + 'action' => function (string $path, string $filename) { + return $this->file($path, $filename)->changeName($this->requestBody('name')); + } + ], + +]; diff --git a/kirby/config/api/routes/languages.php b/kirby/config/api/routes/languages.php new file mode 100755 index 0000000..8d8829b --- /dev/null +++ b/kirby/config/api/routes/languages.php @@ -0,0 +1,46 @@ + '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 new file mode 100755 index 0000000..302a945 --- /dev/null +++ b/kirby/config/api/routes/lock.php @@ -0,0 +1,99 @@ + '(:all)/lock', + 'method' => 'GET', + 'action' => function (string $path) { + 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(); + } + + throw new Exception([ + 'key' => 'lock.notImplemented', + 'httpCode' => 501 + ]); + } + ], + [ + 'pattern' => '(:all)/lock', + 'method' => 'DELETE', + 'action' => function (string $path) { + if ($lock = $this->parent($path)->lock()) { + return $lock->remove(); + } + + throw new Exception([ + 'key' => 'lock.notImplemented', + 'httpCode' => 501 + ]); + } + ], + [ + 'pattern' => '(:all)/unlock', + 'method' => 'GET', + 'action' => function (string $path) { + 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(); + } + + throw new Exception([ + 'key' => 'lock.notImplemented', + 'httpCode' => 501 + ]); + } + ], + [ + 'pattern' => '(:all)/unlock', + 'method' => 'DELETE', + 'action' => function (string $path) { + if ($lock = $this->parent($path)->lock()) { + return $lock->resolve(); + } + + throw new Exception([ + 'key' => 'lock.notImplemented', + 'httpCode' => 501 + ]); + } + ], +]; diff --git a/kirby/config/api/routes/pages.php b/kirby/config/api/routes/pages.php new file mode 100755 index 0000000..5ecfacc --- /dev/null +++ b/kirby/config/api/routes/pages.php @@ -0,0 +1,119 @@ + '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)/children', + 'method' => 'GET', + 'action' => function (string $id) { + return $this->page($id)->children(); + } + ], + [ + 'pattern' => 'pages/(:any)/children', + 'method' => 'POST', + 'action' => function (string $id) { + return $this->page($id)->createChild($this->requestBody()); + } + ], + [ + 'pattern' => 'pages/(:any)/children/blueprints', + 'method' => 'GET', + 'action' => function (string $id) { + return $this->page($id)->blueprints($this->requestQuery('section')); + } + ], + [ + 'pattern' => 'pages/(:any)/children/search', + 'method' => 'GET|POST', + 'action' => function (string $id) { + $pages = $this->page($id)->children(); + + if ($this->requestMethod() === 'GET') { + return $pages->search($this->requestQuery('q')); + } else { + return $pages->query($this->requestBody()); + } + } + ], + [ + '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 new file mode 100755 index 0000000..ab9505b --- /dev/null +++ b/kirby/config/api/routes/roles.php @@ -0,0 +1,28 @@ + '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); + } + ] +]; diff --git a/kirby/config/api/routes/site.php b/kirby/config/api/routes/site.php new file mode 100755 index 0000000..1f64bb5 --- /dev/null +++ b/kirby/config/api/routes/site.php @@ -0,0 +1,96 @@ + '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->site()->children(); + } + ], + [ + 'pattern' => 'site/children', + 'method' => 'POST', + 'action' => function () { + return $this->site()->createChild($this->requestBody()); + } + ], + [ + 'pattern' => 'site/children/blueprints', + 'method' => 'GET', + 'action' => function () { + return $this->site()->blueprints($this->requestQuery('section')); + } + ], + [ + 'pattern' => 'site/children/search', + 'method' => 'POST', + 'action' => function () { + return $this->site()->children()->query($this->requestBody()); + } + ], + [ + '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) + ->filterBy('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); + } + ] + +]; diff --git a/kirby/config/api/routes/system.php b/kirby/config/api/routes/system.php new file mode 100755 index 0000000..44c8807 --- /dev/null +++ b/kirby/config/api/routes/system.php @@ -0,0 +1,79 @@ + '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(); + } + + 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'); + } + + 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->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')); + + 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 new file mode 100755 index 0000000..db7faca --- /dev/null +++ b/kirby/config/api/routes/translations.php @@ -0,0 +1,24 @@ + '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 new file mode 100755 index 0000000..318a571 --- /dev/null +++ b/kirby/config/api/routes/users.php @@ -0,0 +1,141 @@ + 'users', + 'method' => 'GET', + 'action' => function () { + return $this->users(); + } + ], + [ + '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' => 'users/(:any)', + 'method' => 'GET', + 'action' => function (string $id) { + return $this->user($id); + } + ], + [ + 'pattern' => 'users/(:any)', + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->user($id)->update($this->requestBody(), $this->language(), true); + } + ], + [ + 'pattern' => 'users/(:any)', + 'method' => 'DELETE', + 'action' => function (string $id) { + return $this->user($id)->delete(); + } + ], + [ + 'pattern' => 'users/(:any)/avatar', + 'method' => 'GET', + 'action' => function (string $id) { + return $this->user($id)->avatar(); + } + ], + [ + 'pattern' => '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); + } + ], + [ + 'pattern' => 'users/(:any)/avatar', + 'method' => 'DELETE', + 'action' => function (string $id) { + return $this->user($id)->avatar()->delete(); + } + ], + [ + 'pattern' => 'users/(:any)/email', + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->user($id)->changeEmail($this->requestBody('email')); + } + ], + [ + 'pattern' => 'users/(:any)/language', + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->user($id)->changeLanguage($this->requestBody('language')); + } + ], + [ + 'pattern' => 'users/(:any)/name', + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->user($id)->changeName($this->requestBody('name')); + } + ], + [ + 'pattern' => 'users/(:any)/password', + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->user($id)->changePassword($this->requestBody('password')); + } + ], + [ + 'pattern' => 'users/(:any)/role', + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->user($id)->changeRole($this->requestBody('role')); + } + ], + [ + 'pattern' => 'users/(:any)/sections/(:any)', + 'method' => 'GET', + 'action' => function (string $id, string $sectionName) { + if ($section = $this->user($id)->blueprint()->section($sectionName)) { + return $section->toResponse(); + } + } + ], + [ + 'pattern' => 'users/(:any)/fields/(:any)/(:all?)', + 'method' => 'ALL', + 'action' => function (string $id, string $fieldName, string $path = null) { + if ($user = $this->user($id)) { + return $this->fieldApi($user, $fieldName, $path); + } + } + ] + +]; diff --git a/kirby/config/blueprints.php b/kirby/config/blueprints.php new file mode 100755 index 0000000..84792fb --- /dev/null +++ b/kirby/config/blueprints.php @@ -0,0 +1,7 @@ + __DIR__ . '/blueprints/file.yml', + 'pages/default' => __DIR__ . '/blueprints/page.yml', + 'site' => __DIR__ . '/blueprints/site.yml' +]; diff --git a/kirby/config/blueprints/file.yml b/kirby/config/blueprints/file.yml new file mode 100755 index 0000000..d5ef1df --- /dev/null +++ b/kirby/config/blueprints/file.yml @@ -0,0 +1,2 @@ +name: File +title: file diff --git a/kirby/config/blueprints/page.yml b/kirby/config/blueprints/page.yml new file mode 100755 index 0000000..ceb895a --- /dev/null +++ b/kirby/config/blueprints/page.yml @@ -0,0 +1,3 @@ +name: Page +title: Page + diff --git a/kirby/config/blueprints/site.yml b/kirby/config/blueprints/site.yml new file mode 100755 index 0000000..04718f3 --- /dev/null +++ b/kirby/config/blueprints/site.yml @@ -0,0 +1,7 @@ +name: Site +title: Site +sections: + pages: + headline: Pages + type: pages + diff --git a/kirby/config/components.php b/kirby/config/components.php new file mode 100755 index 0000000..776e7ed --- /dev/null +++ b/kirby/config/components.php @@ -0,0 +1,232 @@ + function (App $kirby, string $url, $options = null): string { + return $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) . '
'; + } + + if ($echo === true) { + echo $output; + } + + return $output; + }, + + /** + * 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\Cms\FileModifications $file The file object + * @param array $options All thumb options (width, height, crop, blur, grayscale) + * @return \Kirby\Cms\File|\Kirby\Cms\FileVersion + */ + 'file::version' => function (App $kirby, $file, array $options = []) { + if ($file->isResizable() === false) { + return $file; + } + + // create url and root + $mediaRoot = dirname($file->mediaRoot()); + $dst = $mediaRoot . '/{{ name }}{{ attributes }}.{{ extension }}'; + $thumbRoot = (new Filename($file->root(), $dst, $options))->toString(); + $thumbName = basename($thumbRoot); + $job = $mediaRoot . '/.jobs/' . $thumbName . '.json'; + + if (file_exists($thumbRoot) === false) { + try { + Data::write($job, array_merge($options, [ + 'filename' => $file->filename() + ])); + } catch (Throwable $e) { + return $file; + } + } + + 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' => function (App $kirby, string $url, $options = null): string { + return $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 + * @return string + */ + 'markdown' => function (App $kirby, string $text = null, array $options = [], bool $inline = false): string { + static $markdown; + 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) { + $markdown = new Markdown($options); + $config = $options; + } + + return $markdown->parse($text, $inline); + }, + + /** + * 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; + } + + 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); + + foreach ($snippets as $name) { + $name = (string)$name; + $file = $kirby->root('snippets') . '/' . $name . '.php'; + + if (file_exists($file) === false) { + $file = $kirby->extensions('snippets')[$name] ?? null; + } + + if ($file) { + break; + } + } + + return Snippet::load($file, $data); + }, + + /** + * 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 The root of the original file + * @param string $dst 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(); + + F::copy($src, $root, true); + $darkroom->process($root, $options); + + return $root; + }, + + /** + * Modify all URLs + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param string $path URL path + * @param array|null $options Array of options for the Uri class + * @param Closure $originalHandler Callback function to the original URL handler with `$path` and `$options` as parameters + * @return string + */ + 'url' => function (App $kirby, string $path = null, $options = [], Closure $originalHandler): string { + return $originalHandler($path, $options); + }, + +]; diff --git a/kirby/config/fields.php b/kirby/config/fields.php new file mode 100755 index 0000000..04fb325 --- /dev/null +++ b/kirby/config/fields.php @@ -0,0 +1,27 @@ + __DIR__ . '/fields/checkboxes.php', + 'date' => __DIR__ . '/fields/date.php', + 'email' => __DIR__ . '/fields/email.php', + 'files' => __DIR__ . '/fields/files.php', + 'headline' => __DIR__ . '/fields/headline.php', + 'hidden' => __DIR__ . '/fields/hidden.php', + 'info' => __DIR__ . '/fields/info.php', + 'line' => __DIR__ . '/fields/line.php', + 'multiselect' => __DIR__ . '/fields/multiselect.php', + 'number' => __DIR__ . '/fields/number.php', + 'pages' => __DIR__ . '/fields/pages.php', + 'radio' => __DIR__ . '/fields/radio.php', + 'range' => __DIR__ . '/fields/range.php', + 'select' => __DIR__ . '/fields/select.php', + 'structure' => __DIR__ . '/fields/structure.php', + 'tags' => __DIR__ . '/fields/tags.php', + 'tel' => __DIR__ . '/fields/tel.php', + 'text' => __DIR__ . '/fields/text.php', + 'textarea' => __DIR__ . '/fields/textarea.php', + 'time' => __DIR__ . '/fields/time.php', + 'toggle' => __DIR__ . '/fields/toggle.php', + 'url' => __DIR__ . '/fields/url.php', + 'users' => __DIR__ . '/fields/users.php' +]; diff --git a/kirby/config/fields/checkboxes.php b/kirby/config/fields/checkboxes.php new file mode 100755 index 0000000..6837b45 --- /dev/null +++ b/kirby/config/fields/checkboxes.php @@ -0,0 +1,61 @@ + ['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' + ] +]; diff --git a/kirby/config/fields/date.php b/kirby/config/fields/date.php new file mode 100755 index 0000000..cbf4c55 --- /dev/null +++ b/kirby/config/fields/date.php @@ -0,0 +1,129 @@ + [ + /** + * Default date when a new page/file/user gets created + */ + 'default' => function ($default = null) { + return $default; + }, + + /** + * Defines a custom format that is used when the field is saved + */ + 'format' => function (string $format = null) { + return $format; + }, + + /** + * Changes the calendar icon to something custom + */ + 'icon' => function (string $icon = 'calendar') { + return $icon; + }, + /** + * Youngest date, which can be selected/saved + */ + 'max' => function (string $max = null) { + return $this->toDate($max); + }, + /** + * Oldest date, which can be selected/saved + */ + 'min' => function (string $min = null) { + return $this->toDate($min); + }, + /** + * The placeholder is not available + */ + 'placeholder' => null, + /** + * 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' => [ + 'default' => function () { + return $this->toDate($this->default); + }, + 'format' => function () { + return $this->props['format'] ?? ($this->time() === false ? 'Y-m-d' : 'Y-m-d H:i'); + }, + 'value' => function () { + return $this->toDate($this->value); + }, + ], + 'methods' => [ + 'toDate' => function ($value) { + if ($timestamp = timestamp($value, $this->time['step'] ?? 5)) { + return date('Y-m-d H:i:s', $timestamp); + } + + return null; + } + ], + 'save' => function ($value) { + if ($value !== null && $date = strtotime($value)) { + return date($this->format(), $date); + } + + return ''; + }, + 'validations' => [ + 'date', + 'minMax' => function ($value) { + $min = $this->min ? strtotime($this->min) : null; + $max = $this->max ? strtotime($this->max) : null; + $value = strtotime($this->value()); + $format = 'd.m.Y'; + $errors = []; + + if ($value && $min && $value < $min) { + $errors['min'] = $min; + } + + if ($value && $max && $value > $max) { + $errors['max'] = $max; + } + + if (empty($errors) === false) { + if ($min && $max) { + throw new Exception([ + 'key' => 'validation.date.between', + 'data' => [ + 'min' => date($format, $min), + 'max' => date($format, $max) + ] + ]); + } elseif ($min) { + throw new Exception([ + 'key' => 'validation.date.after', + 'data' => [ + 'date' => date($format, $min), + ] + ]); + } else { + throw new Exception([ + 'key' => 'validation.date.before', + 'data' => [ + 'date' => date($format, $max), + ] + ]); + } + } + + return true; + }, + ] +]; diff --git a/kirby/config/fields/email.php b/kirby/config/fields/email.php new file mode 100755 index 0000000..e7892b8 --- /dev/null +++ b/kirby/config/fields/email.php @@ -0,0 +1,40 @@ + 'text', + 'props' => [ + /** + * Unset inherited props + */ + 'converter' => null, + 'counter' => null, + + /** + * 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; + }, + + /** + * 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 new file mode 100755 index 0000000..9ecb0c1 --- /dev/null +++ b/kirby/config/fields/files.php @@ -0,0 +1,138 @@ + [ + 'picker', + 'filepicker', + 'min', + '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; + }, + + /** + * Changes the layout of the selected files. Available layouts: `list`, `cards` + */ + 'layout' => function (string $layout = 'list') { + return $layout; + }, + + /** + * Layout size for cards: `tiny`, `small`, `medium`, `large` or `huge` + */ + 'size' => function (string $size = 'auto') { + return $size; + }, + + '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->panelPickerData([ + 'image' => $this->image, + 'info' => $this->info ?? false, + 'model' => $this->model(), + 'text' => $this->text, + ]); + }, + 'toFiles' => function ($value = null) { + $files = []; + + foreach (Yaml::decode($value) 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); + } + } + + return $files; + } + ], + 'api' => function () { + return [ + [ + 'pattern' => '/', + 'action' => function () { + $field = $this->field(); + + return $field->filepicker([ + 'image' => $field->image(), + 'info' => $field->info(), + '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->upload($this, $uploads, function ($file, $parent) use ($field) { + return $file->panelPickerData([ + 'image' => $field->image(), + 'info' => $field->info(), + 'model' => $field->model(), + 'text' => $field->text(), + ]); + }); + } + ] + ]; + }, + 'save' => function ($value = null) { + return A::pluck($value, 'uuid'); + }, + 'validations' => [ + 'max', + 'min' + ] +]; diff --git a/kirby/config/fields/headline.php b/kirby/config/fields/headline.php new file mode 100755 index 0000000..9b52938 --- /dev/null +++ b/kirby/config/fields/headline.php @@ -0,0 +1,27 @@ + false, + 'props' => [ + /** + * Unset inherited props + */ + 'after' => null, + 'autofocus' => null, + 'before' => null, + 'default' => null, + 'disabled' => null, + 'help' => null, + 'icon' => null, + 'placeholder' => null, + 'required' => null, + 'translate' => null, + + /** + * If `false`, the prepended number will be hidden + */ + 'numbered' => function (bool $numbered = true) { + return $numbered; + } + ] +]; diff --git a/kirby/config/fields/hidden.php b/kirby/config/fields/hidden.php new file mode 100755 index 0000000..0b67a5f --- /dev/null +++ b/kirby/config/fields/hidden.php @@ -0,0 +1,3 @@ + [ + /** + * Text to be displayed + */ + 'text' => function ($value = null) { + return I18n::translate($value, $value); + }, + ], + 'computed' => [ + 'text' => function () { + if ($text = $this->text) { + $text = $this->model()->toString($text); + $text = $this->kirby()->kirbytext($text); + return $text; + } + } + ], + 'save' => false, +]; diff --git a/kirby/config/fields/line.php b/kirby/config/fields/line.php new file mode 100755 index 0000000..6844d6c --- /dev/null +++ b/kirby/config/fields/line.php @@ -0,0 +1,5 @@ + false +]; diff --git a/kirby/config/fields/mixins/filepicker.php b/kirby/config/fields/mixins/filepicker.php new file mode 100755 index 0000000..ba81230 --- /dev/null +++ b/kirby/config/fields/mixins/filepicker.php @@ -0,0 +1,14 @@ + [ + 'filepicker' => function (array $params = []) { + // fetch the parent model + $params['model'] = $this->model(); + + return (new FilePicker($params))->toArray(); + } + ] +]; diff --git a/kirby/config/fields/mixins/min.php b/kirby/config/fields/mixins/min.php new file mode 100755 index 0000000..33e24d4 --- /dev/null +++ b/kirby/config/fields/mixins/min.php @@ -0,0 +1,22 @@ + [ + '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->required; + } + ] +]; diff --git a/kirby/config/fields/mixins/options.php b/kirby/config/fields/mixins/options.php new file mode 100755 index 0000000..170761a --- /dev/null +++ b/kirby/config/fields/mixins/options.php @@ -0,0 +1,48 @@ + [ + /** + * 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 new file mode 100755 index 0000000..bbdc86e --- /dev/null +++ b/kirby/config/fields/mixins/pagepicker.php @@ -0,0 +1,14 @@ + [ + 'pagepicker' => function (array $params = []) { + // inject the current model + $params['model'] = $this->model(); + + return (new PagePicker($params))->toArray(); + } + ] +]; diff --git a/kirby/config/fields/mixins/picker.php b/kirby/config/fields/mixins/picker.php new file mode 100755 index 0000000..a04ac95 --- /dev/null +++ b/kirby/config/fields/mixins/picker.php @@ -0,0 +1,71 @@ + [ + /** + * 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; + }, + + /** + * Info text for each item + */ + 'info' => function (string $info = null) { + return $info; + }, + + /** + * 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; + }, + + /** + * 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; + }, + + /** + * 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; + }, + + ], +]; diff --git a/kirby/config/fields/mixins/upload.php b/kirby/config/fields/mixins/upload.php new file mode 100755 index 0000000..ce5bd4c --- /dev/null +++ b/kirby/config/fields/mixins/upload.php @@ -0,0 +1,72 @@ + [ + /** + * 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_array($uploads) === false) { + $uploads = []; + } + + $template = $uploads['template'] ?? null; + + if ($template) { + $file = new File([ + 'filename' => 'tmp', + 'template' => $template + ]); + + $uploads['accept'] = $file->blueprint()->accept()['mime'] ?? '*'; + } 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'); + } + + 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(); + } + + 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'); + } + + return $map($file, $parent); + }); + } + ] +]; diff --git a/kirby/config/fields/mixins/userpicker.php b/kirby/config/fields/mixins/userpicker.php new file mode 100755 index 0000000..41c2b62 --- /dev/null +++ b/kirby/config/fields/mixins/userpicker.php @@ -0,0 +1,13 @@ + [ + 'userpicker' => function (array $params = []) { + $params['model'] = $this->model(); + + return (new UserPicker($params))->toArray(); + } + ] +]; diff --git a/kirby/config/fields/multiselect.php b/kirby/config/fields/multiselect.php new file mode 100755 index 0000000..122d0c2 --- /dev/null +++ b/kirby/config/fields/multiselect.php @@ -0,0 +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 + */ + 'search' => function (bool $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 new file mode 100755 index 0000000..6f78f77 --- /dev/null +++ b/kirby/config/fields/number.php @@ -0,0 +1,48 @@ + [ + /** + * 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 (float)Str::float($value); + } + ], + 'validations' => [ + 'min', + 'max' + ] +]; diff --git a/kirby/config/fields/pages.php b/kirby/config/fields/pages.php new file mode 100755 index 0000000..9b4a3c2 --- /dev/null +++ b/kirby/config/fields/pages.php @@ -0,0 +1,117 @@ + ['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); + }, + + /** + * Changes the layout of the selected files. Available layouts: `list`, `cards` + */ + 'layout' => function (string $layout = 'list') { + return $layout; + }, + + /** + * Optional query to select a specific set of pages + */ + 'query' => function (string $query = null) { + return $query; + }, + + /** + * Layout size for cards: `tiny`, `small`, `medium`, `large` or `huge` + */ + 'size' => function (string $size = 'auto') { + return $size; + }, + + /** + * 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->panelPickerData([ + 'image' => $this->image, + 'info' => $this->info, + 'text' => $this->text, + ]); + }, + 'toPages' => function ($value = null) { + $pages = []; + $kirby = kirby(); + + foreach (Yaml::decode($value) as $id) { + if (is_array($id) === true) { + $id = $id['id'] ?? null; + } + + if ($id !== null && ($page = $kirby->page($id))) { + $pages[] = $this->pageResponse($page); + } + } + + return $pages; + } + ], + 'api' => function () { + return [ + [ + 'pattern' => '/', + 'action' => function () { + $field = $this->field(); + + return $field->pagepicker([ + 'image' => $field->image(), + 'info' => $field->info(), + '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 new file mode 100755 index 0000000..dd9ffc3 --- /dev/null +++ b/kirby/config/fields/radio.php @@ -0,0 +1,29 @@ + ['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) ?? ''; + } + ] +]; diff --git a/kirby/config/fields/range.php b/kirby/config/fields/range.php new file mode 100755 index 0000000..5f14388 --- /dev/null +++ b/kirby/config/fields/range.php @@ -0,0 +1,24 @@ + '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; + }, + ] +]; diff --git a/kirby/config/fields/select.php b/kirby/config/fields/select.php new file mode 100755 index 0000000..24b14b6 --- /dev/null +++ b/kirby/config/fields/select.php @@ -0,0 +1,24 @@ + '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; + }, + ] +]; diff --git a/kirby/config/fields/structure.php b/kirby/config/fields/structure.php new file mode 100755 index 0000000..1eb27ba --- /dev/null +++ b/kirby/config/fields/structure.php @@ -0,0 +1,179 @@ + ['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); + }, + /** + * 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; + }, + + /** + * 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 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 = []; + + if (empty($this->columns)) { + foreach ($this->fields as $field) { + + // Skip hidden fields. + // They should never be included as column + if ($field['type'] === 'hidden') { + 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 = []; + } + + $field = $this->fields[$columnName] ?? null; + + if (empty($field) === true) { + continue; + } + + $columns[$columnName] = array_merge($columnProps, [ + 'type' => $field['type'], + 'label' => $field['label'] ?? $field['name'] + ]); + } + } + + return $columns; + } + ], + 'methods' => [ + 'rows' => function ($value) { + $rows = Yaml::decode($value); + $value = []; + + foreach ($rows as $index => $row) { + if (is_array($row) === false) { + continue; + } + + $value[] = $this->form($row)->values(); + } + + 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 = []; + + foreach ($value as $row) { + $data[] = $this->form($row)->data(); + } + + return $data; + }, + 'validations' => [ + 'min', + 'max' + ] +]; diff --git a/kirby/config/fields/tags.php b/kirby/config/fields/tags.php new file mode 100755 index 0000000..93c29bd --- /dev/null +++ b/kirby/config/fields/tags.php @@ -0,0 +1,96 @@ + ['min', 'options'], + 'props' => [ + + /** + * 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; + }, + /** + * 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(); + + // 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; + } + + $index = array_search($option, array_column($options, 'value')); + + 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' + ] +]; diff --git a/kirby/config/fields/tel.php b/kirby/config/fields/tel.php new file mode 100755 index 0000000..3d73430 --- /dev/null +++ b/kirby/config/fields/tel.php @@ -0,0 +1,27 @@ + 'text', + 'props' => [ + /** + * Unset inherited props + */ + 'converter' => null, + 'counter' => null, + 'spellcheck' => null, + + /** + * Sets the HTML5 autocomplete attribute + */ + 'autocomplete' => function (string $autocomplete = 'tel') { + return $autocomplete; + }, + + /** + * 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 new file mode 100755 index 0000000..c32a037 --- /dev/null +++ b/kirby/config/fields/text.php @@ -0,0 +1,103 @@ + [ + + /** + * 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; + }, + + /** + * 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; + }, + + /** + * 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; + }, + + /** + * 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()]; + + 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' + ] +]; diff --git a/kirby/config/fields/textarea.php b/kirby/config/fields/textarea.php new file mode 100755 index 0000000..cd23ddf --- /dev/null +++ b/kirby/config/fields/textarea.php @@ -0,0 +1,122 @@ + ['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 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 options for the files picker + */ + 'files' => function ($files = []) { + if (is_string($files) === true) { + return ['query' => $files]; + } + + if (is_array($files) === false) { + $files = []; + } + + return $files; + }, + + /** + * 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; + }, + + /** + * 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; + }, + + /** + * 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') + ]); + + return $this->field()->filepicker($params); + } + ], + [ + 'pattern' => 'upload', + '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 [ + 'filename' => $file->filename(), + 'dragText' => $file->dragText('auto', $absolute), + ]; + }); + } + ] + ]; + }, + 'validations' => [ + 'minlength', + 'maxlength' + ] +]; diff --git a/kirby/config/fields/time.php b/kirby/config/fields/time.php new file mode 100755 index 0000000..ef2fa23 --- /dev/null +++ b/kirby/config/fields/time.php @@ -0,0 +1,68 @@ + [ + /** + * Unset inherited props + */ + 'placeholder' => null, + + /** + * Sets the default time when a new page/file/user is created + */ + 'default' => function ($default = null) { + return $default; + }, + /** + * Changes the clock icon + */ + 'icon' => function (string $icon = 'clock') { + return $icon; + }, + /** + * `12` or `24` hour notation. If `12`, an AM/PM selector will be shown. + */ + 'notation' => function (int $value = 24) { + return $value === 24 ? 24 : 12; + }, + /** + * The interval between minutes in the minutes select dropdown. + */ + 'step' => function (int $step = 5) { + return $step; + }, + 'value' => function ($value = null) { + return $value; + } + ], + 'computed' => [ + 'default' => function () { + return $this->toTime($this->default); + }, + 'format' => function () { + return $this->notation === 24 ? 'H:i' : 'h:i a'; + }, + 'value' => function () { + return $this->toTime($this->value); + } + ], + 'methods' => [ + 'toTime' => function ($value) { + if ($timestamp = timestamp($value, $this->step)) { + return date('H:i', $timestamp); + } + + return null; + } + ], + 'save' => function ($value): string { + if ($timestamp = strtotime($value)) { + return date($this->format, $timestamp); + } + + return ''; + }, + 'validations' => [ + 'time', + ] +]; diff --git a/kirby/config/fields/toggle.php b/kirby/config/fields/toggle.php new file mode 100755 index 0000000..a43c613 --- /dev/null +++ b/kirby/config/fields/toggle.php @@ -0,0 +1,68 @@ + [ + /** + * 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) { + if (is_array($value) === true) { + if (A::isAssociative($value) === true) { + return I18n::translate($value, $value); + } + + foreach ($value as $key => $val) { + $value[$key] = I18n::translate($val, $val); + } + + return $value; + } + + return I18n::translate($value, $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([ + 'key' => 'form.field.required' + ]); + } + }, + ] +]; diff --git a/kirby/config/fields/url.php b/kirby/config/fields/url.php new file mode 100755 index 0000000..f92dd2c --- /dev/null +++ b/kirby/config/fields/url.php @@ -0,0 +1,41 @@ + 'text', + 'props' => [ + /** + * Unset inherited props + */ + 'converter' => null, + 'counter' => null, + 'spellcheck' => null, + + /** + * Sets the HTML5 autocomplete attribute + */ + 'autocomplete' => function (string $autocomplete = 'url') { + return $autocomplete; + }, + + /** + * 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' + ], +]; diff --git a/kirby/config/fields/users.php b/kirby/config/fields/users.php new file mode 100755 index 0000000..bc96bd4 --- /dev/null +++ b/kirby/config/fields/users.php @@ -0,0 +1,97 @@ + ['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 []; + } + + if ($default === null && $user = $this->kirby()->user()) { + return [ + $this->userResponse($user) + ]; + } + + return $this->toUsers($default); + }, + + 'value' => function ($value = null) { + return $this->toUsers($value); + }, + ], + 'computed' => [ + /** + * Unset inherited computed + */ + 'default' => null + ], + 'methods' => [ + 'userResponse' => function ($user) { + return $user->panelPickerData([ + 'info' => $this->info, + 'image' => $this->image, + 'text' => $this->text, + ]); + }, + 'toUsers' => function ($value = null) { + $users = []; + $kirby = kirby(); + + foreach (Yaml::decode($value) as $email) { + if (is_array($email) === true) { + $email = $email['email'] ?? null; + } + + if ($email !== null && ($user = $kirby->user($email))) { + $users[] = $this->userResponse($user); + } + } + + return $users; + } + ], + 'api' => function () { + return [ + [ + 'pattern' => '/', + 'action' => function () { + $field = $this->field(); + + return $field->userpicker([ + 'image' => $field->image(), + 'info' => $field->info(), + '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/helpers.php b/kirby/config/helpers.php new file mode 100755 index 0000000..23ca726 --- /dev/null +++ b/kirby/config/helpers.php @@ -0,0 +1,870 @@ +collection($name); +} + +/** + * Checks / returns a CSRF token + * + * @param string $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(); + + // 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) { + // no arguments, generate/return a token + + $token = $session->get('csrf'); + if (is_string($token) !== true) { + $token = bin2hex(random_bytes(32)); + $session->set('csrf', $token); + } + + return $token; + } elseif (is_string($check) === true && is_string($session->get('csrf')) === true) { + // argument has been passed, check the token + return hash_equals($session->get('csrf'), $check) === true; + } + + return false; +} + +/** + * 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 = array_map(function ($url) use ($options) { + return css($url, $options); + }, $url); + + 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; + } + } + + $url = $kirby->component('css')($kirby, $url, $options); + $url = Url::to($url); + $attr = array_merge((array)$options, [ + 'href' => $url, + 'rel' => 'stylesheet' + ]); + + return ''; +} + +/** + * 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; +} + +/** + * 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); +} + +/** + * 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); +} + +/** + * Escape context specific output + * + * @param string $string Untrusted data + * @param string $context Location of output + * @param bool $strict Whether to escape an extended set of characters (HTML attributes only) + * @return string Escaped data + */ +function esc($string, $context = 'html', $strict = false) +{ + if (method_exists('Kirby\Toolkit\Escape', $context) === true) { + return Escape::$context($string, $strict); + } + + return $string; +} + + +/** + * 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 $file + * @return string + */ +function gist(string $url, string $file = null): string +{ + return 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 = null, int $code = 302) +{ + die(Response::redirect($url, $code)); +} + +/** + * Shortcut for html() + * + * @param string $string unencoded text + * @param bool $keepTags + * @return string + */ +function h(string $string = null, bool $keepTags = false) +{ + return Html::encode($string, $keepTags); +} + +/** + * Creates safe html by encoding special characters + * + * @param string $string unencoded text + * @param bool $keepTags + * @return string + */ +function html(string $string = null, bool $keepTags = false) +{ + return Html::encode($string, $keepTags); +} + +/** + * Return an image from any page + * specified by the path + * + * Example: + * + * + * @param string $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; + } +} + +/** + * 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 false|array + */ +function invalid(array $data = [], array $rules = [], array $messages = []) +{ + $errors = []; + + foreach ($rules as $field => $validations) { + $validationIndex = -1; + + // See: http://php.net/manual/en/types.comparisons.php + // only false for: null, undefined variable, '', [] + $filled = isset($data[$field]) && $data[$field] !== '' && $data[$field] !== []; + $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 (is_numeric($method) === true) { + $method = $options; + } + + $validationIndex++; + + if ($method === 'required') { + if ($filled) { + // Field is required and filled. + continue; + } + } elseif ($filled) { + if (is_array($options) === false) { + $options = [$options]; + } + + array_unshift($options, $data[$field] ?? null); + + 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; +} + +/** + * 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 = array_map(function ($url) use ($options) { + return js($url, $options); + }, $url); + + 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 ''; +} + +/** + * 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 $value + * @param array $attr + * @return string + */ +function kirbytag($type, string $value = null, array $attr = []): string +{ + if (is_array($type) === true) { + return App::instance()->kirbytag(key($type), current($type), $type); + } + + return App::instance()->kirbytag($type, $value, $attr); +} + +/** + * Parses KirbyTags in the given string. Shortcut + * for `$kirby->kirbytags($text, $data)` + * + * @param string $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 $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 $text + * @param array $data + * @return string + */ +function kirbytextinline(string $text = null, array $data = []): string +{ + return App::instance()->kirbytext($text, $data, true); +} + +/** + * Shortcut for `kirbytext()` helper + * + * @param string $text + * @param array $data + * @return string + */ +function kt(string $text = null, array $data = []): string +{ + return kirbytext($text, $data); +} + +/** + * Shortcut for `kirbytextinline()` helper + * @since 3.1.0 + * + * @param string $text + * @param array $data + * @return string + */ +function kti(string $text = null, array $data = []): string +{ + return kirbytextinline($text, $data); +} + +/** + * A super simple class autoloader + * + * @param array $classmap + * @param string $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]; + } + }); +} + +/** + * Parses markdown in the given string. Shortcut for + * `$kirby->markdown($text)` + * + * @param string $text + * @return string + */ +function markdown(string $text = null): string +{ + return App::instance()->markdown($text); +} + +/** + * 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|null + */ +function page(...$id) +{ + if (empty($id) === true) { + return App::instance()->site()->page(); + } + + return App::instance()->site()->find(...$id); +} + +/** + * Helper to build page collections + * + * @param string|array ...$id + * @return \Kirby\Cms\Pages + */ +function pages(...$id) +{ + return App::instance()->site()->find(...$id); +} + +/** + * Returns a single param from the URL + * + * @param string $key + * @param string $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(); +} + +/** + * 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; +} + +/** + * Rounds the minutes of the given date + * by the defined step + * + * @param string $date + * @param int $step + * @return string|null + */ +function timestamp(string $date = null, int $step = null): ?string +{ + if (V::date($date) === false) { + return null; + } + + $date = strtotime($date); + + if ($step === null) { + return $date; + } + + $hours = date('H', $date); + $minutes = date('i', $date); + $minutes = floor($minutes / $step) * $step; + $minutes = str_pad($minutes, 2, 0, STR_PAD_LEFT); + $date = date('Y-m-d', $date) . ' ' . $hours . ':' . $minutes; + + return strtotime($date); +} + +/** + * Returns the currrent 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 + */ +function size($value): int +{ + if (is_numeric($value)) { + return $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(); + } + } +} + +/** + * Enhances the given string with + * smartypants. Shortcut for `$kirby->smartypants($text)` + * + * @param string $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; +} + +/** + * 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); + + if (file_exists($file) === false) { + return false; + } + } + + return F::read($file); +} + +/** + * Returns translate string for key from translation file + * + * @param string|array $key + * @param string|null $fallback + * @return mixed + */ +function t($key, string $fallback = null) +{ + return I18n::translate($key, $fallback); +} + +/** + * Translates a count + * + * @param string|array $key + * @param int $count + * @return mixed + */ +function tc($key, int $count) +{ + return I18n::translateCount($key, $count); +} + +/** + * Translate by key and then replace + * placeholders in the text + * + * @param string $key + * @param string $fallback + * @param array $replace + * @param string $locale + * @return string + */ +function tt(string $key, $fallback = null, array $replace = null, string $locale = null) +{ + return I18n::template($key, $fallback, $replace, $locale); +} + +/** + * Builds a Twitter link + * + * @param string $username + * @param string $text + * @param string $title + * @param string $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 + ]); +} + +/** + * Shortcut for url() + * + * @param string $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 $path + * @param array|string|null $options + * @return string + */ +function url(string $path = null, $options = null): string +{ + return Url::to($path, $options); +} + +/** + * 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 + */ +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 + */ +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); +} + +/** + * Embeds a Youtube video by URL in an iframe + * + * @param string $url + * @param array $options + * @param array $attr + * @return string + */ +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 new file mode 100755 index 0000000..dc0d963 --- /dev/null +++ b/kirby/config/methods.php @@ -0,0 +1,520 @@ + 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; + }, + + /** + * 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 + + /** + * 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': + return Yaml::decode($field->value); + case 'json': + return Json::decode($field->value); + default: + return $field->split($method); + } + }, + + /** + * 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 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; + } + + $time = empty($field->value) === true ? strtotime($fallback) : $field->toTimestamp(); + + if ($format === null) { + return $time; + } + + return $app->option('date.handler', 'date')($format, $time); + }, + + /** + * 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([]); + + foreach ($field->toData($separator) as $id) { + if ($file = $parent->kirby()->file($id, $parent)) { + $files->add($file); + } + } + + 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 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; + }, + + /** + * 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'; + } + + 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 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(Yaml::decode($field->value), $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); + } + }, + + /** + * 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); + }, + + /** + * 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)); + }, + + // inspectors + + /** + * 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)); + }, + + // manipulators + + /** + * 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 html, attr, js or css + */ + 'escape' => function (Field $field, string $context = 'html') { + $field->value = 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; + }, + + /** + * Converts the field content to valid HTML + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Field + */ + 'html' => function (Field $field) { + $field->value = htmlentities($field->value, ENT_COMPAT, 'utf-8'); + 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 the field content from Markdown/Kirbytext to valid HTML + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Field + */ + 'kirbytext' => function (Field $field) use ($app) { + $field->value = $app->kirbytext($field->value, [ + 'parent' => $field->parent(), + 'field' => $field + ]); + + return $field; + }, + + /** + * Converts the field content from inline Markdown/Kirbytext + * to valid HTML + * @since 3.1.0 + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Field + */ + 'kirbytextinline' => function (Field $field) use ($app) { + $field->value = $app->kirbytext($field->value, [ + 'parent' => $field->parent(), + 'field' => $field + ], true); + + 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 + ]); + + 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, '
'); + 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 + * @return \Kirby\Cms\Field + */ + 'markdown' => function (Field $field) use ($app) { + $field->value = $app->markdown($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; + }, + + /** + * 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; + }, + + /** + * 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 \Kirby\cms\Field + */ + '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; + }, + + /** + * 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; + }, + + // 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'); + }, + + ]; +}; diff --git a/kirby/config/presets/files.php b/kirby/config/presets/files.php new file mode 100755 index 0000000..fe0a7e5 --- /dev/null +++ b/kirby/config/presets/files.php @@ -0,0 +1,24 @@ + [ + 'headline' => $props['headline'] ?? t('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'] + ); + + return $props; +}; diff --git a/kirby/config/presets/page.php b/kirby/config/presets/page.php new file mode 100755 index 0000000..62df5de --- /dev/null +++ b/kirby/config/presets/page.php @@ -0,0 +1,72 @@ + $props + ]; + } + + return array_replace_recursive($defaults, $props); + }; + + if (empty($props['sidebar']) === false) { + $sidebar = $props['sidebar']; + } else { + $sidebar = []; + + $pages = $props['pages'] ?? []; + $files = $props['files'] ?? []; + + if ($pages !== false) { + $sidebar['pages'] = $section([ + 'headline' => t('pages'), + 'type' => 'pages', + 'status' => 'all', + 'layout' => 'list', + ], $pages); + } + + if ($files !== false) { + $sidebar['files'] = $section([ + 'headline' => t('files'), + 'type' => 'files', + 'layout' => 'list' + ], $files); + } + } + + 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['fields'], + $props['files'], + $props['pages'], + $props['sidebar'] + ); + } + + return $props; +}; diff --git a/kirby/config/presets/pages.php b/kirby/config/presets/pages.php new file mode 100755 index 0000000..5bba76b --- /dev/null +++ b/kirby/config/presets/pages.php @@ -0,0 +1,57 @@ + $headline, + 'type' => 'pages', + 'layout' => 'list', + 'status' => $status + ]; + + if ($props === true) { + $props = []; + } + + if (is_string($props) === true) { + $props = [ + 'headline' => $props + ]; + } + + // inject the global templates definition + if (empty($templates) === false) { + $props['templates'] = $props['templates'] ?? $templates; + } + + return array_replace_recursive($defaults, $props); + }; + + $sections = []; + + $drafts = $props['drafts'] ?? []; + $unlisted = $props['unlisted'] ?? false; + $listed = $props['listed'] ?? []; + + + if ($drafts !== false) { + $sections['drafts'] = $section(t('pages.status.draft'), 'drafts', $drafts); + } + + if ($unlisted !== false) { + $sections['unlisted'] = $section(t('pages.status.unlisted'), 'unlisted', $unlisted); + } + + if ($listed !== false) { + $sections['listed'] = $section(t('pages.status.listed'), 'listed', $listed); + } + + // cleaning up + unset($props['drafts'], $props['unlisted'], $props['listed'], $props['templates']); + + return array_merge($props, ['sections' => $sections]); +}; diff --git a/kirby/config/roots.php b/kirby/config/roots.php new file mode 100755 index 0000000..c473763 --- /dev/null +++ b/kirby/config/roots.php @@ -0,0 +1,90 @@ + function (array $roots) { + return realpath(__DIR__ . '/../'); + }, + + // i18n + 'i18n' => function (array $roots) { + return $roots['kirby'] . '/i18n'; + }, + 'i18n:translations' => function (array $roots) { + return $roots['i18n'] . '/translations'; + }, + 'i18n:rules' => function (array $roots) { + return $roots['i18n'] . '/rules'; + }, + + // index + 'index' => function (array $roots) { + return realpath(__DIR__ . '/../../'); + }, + + // assets + 'assets' => function (array $roots) { + return $roots['index'] . '/assets'; + }, + + // content + 'content' => function (array $roots) { + return $roots['index'] . '/content'; + }, + + // media + 'media' => function (array $roots) { + return $roots['index'] . '/media'; + }, + + // panel + 'panel' => function (array $roots) { + return $roots['kirby'] . '/panel'; + }, + + // site + 'site' => function (array $roots) { + return $roots['index'] . '/site'; + }, + 'accounts' => function (array $roots) { + return $roots['site'] . '/accounts'; + }, + 'blueprints' => function (array $roots) { + return $roots['site'] . '/blueprints'; + }, + 'cache' => function (array $roots) { + return $roots['site'] . '/cache'; + }, + 'collections' => function (array $roots) { + return $roots['site'] . '/collections'; + }, + 'config' => function (array $roots) { + return $roots['site'] . '/config'; + }, + 'controllers' => function (array $roots) { + return $roots['site'] . '/controllers'; + }, + 'languages' => function (array $roots) { + return $roots['site'] . '/languages'; + }, + 'models' => function (array $roots) { + return $roots['site'] . '/models'; + }, + 'plugins' => function (array $roots) { + return $roots['site'] . '/plugins'; + }, + 'sessions' => function (array $roots) { + return $roots['site'] . '/sessions'; + }, + 'snippets' => function (array $roots) { + return $roots['site'] . '/snippets'; + }, + 'templates' => function (array $roots) { + return $roots['site'] . '/templates'; + }, + + // blueprints + 'roles' => function (array $roots) { + return $roots['blueprints'] . '/users'; + }, +]; diff --git a/kirby/config/routes.php b/kirby/config/routes.php new file mode 100755 index 0000000..6c4a02b --- /dev/null +++ b/kirby/config/routes.php @@ -0,0 +1,151 @@ +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'; + } + + /** + * 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(); + + 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 PanelPlugins(); + + return $kirby + ->response() + ->type($type) + ->body($plugins->read($type)); + } + ], + [ + 'pattern' => $media . '/plugins/(:any)/(:any)/(:all).(css|gif|js|jpg|png|svg|webp|woff2|woff)', + 'env' => 'media', + 'action' => function (string $provider, string $pluginName, string $filename, string $extension) { + return PluginAssets::resolve($provider . '/' . $pluginName, $filename . '.' . $extension); + } + ], + [ + 'pattern' => $panel . '/(:all?)', + 'env' => 'panel', + 'action' => function () use ($kirby) { + if ($kirby->option('panel') === false) { + return null; + } + + return Panel::render($kirby); + } + ], + [ + '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); + } + ] + ]; + + // 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(); + } + ]; + + // 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); + } + ]; + } + + return [ + 'before' => $before, + 'after' => $after + ]; +}; diff --git a/kirby/config/sections/fields.php b/kirby/config/sections/fields.php new file mode 100755 index 0000000..84c9276 --- /dev/null +++ b/kirby/config/sections/fields.php @@ -0,0 +1,66 @@ + [ + 'fields' => function (array $fields = []) { + return $fields; + } + ], + 'computed' => [ + 'form' => function () { + $fields = $this->fields; + $disabled = $this->model->permissions()->update() === false; + $content = $this->model->content()->toArray(); + + 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(); + + 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']); + } + + return $fields; + }, + 'errors' => function () { + return $this->form->errors(); + }, + 'data' => function () { + $values = $this->form->values(); + + 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($values['title']); + } + + return $values; + } + ], + 'toArray' => function () { + return [ + 'errors' => $this->errors, + 'fields' => $this->fields, + ]; + } +]; diff --git a/kirby/config/sections/files.php b/kirby/config/sections/files.php new file mode 100755 index 0000000..1d15ca8 --- /dev/null +++ b/kirby/config/sections/files.php @@ -0,0 +1,236 @@ + [ + '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) or below (cards) the filename. + */ + 'info' => function (string $info = null) { + return $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 (string $text = '{{ file.filename }}') { + return $text; + } + ], + 'computed' => [ + 'accept' => function () { + if ($this->template) { + $file = new File([ + 'filename' => 'tmp', + 'template' => $this->template + ]); + + return $file->blueprint()->accept()['mime'] ?? '*'; + } + + return null; + }, + 'parent' => function () { + return $this->parentModel(); + }, + 'files' => function () { + $files = $this->parent->files()->template($this->template); + + if ($this->sortBy) { + $files = $files->sortBy(...$files::sortArgs($this->sortBy)); + } elseif ($this->sortable === true) { + $files = $files->sortBy('sort', 'asc', 'filename', 'asc'); + } + + // flip + if ($this->flip === true) { + $files = $files->flip(); + } + + // apply the default pagination + $files = $files->paginate([ + 'page' => $this->page, + 'limit' => $this->limit + ]); + + return $files; + }, + 'data' => function () { + $data = []; + + // the drag text needs to be absolute when the files come from + // a different parent model + $dragTextAbsolute = $this->model->is($this->parent) === false; + + foreach ($this->files as $file) { + $image = $file->panelImage($this->image); + + $data[] = [ + 'dragText' => $file->dragText('auto', $dragTextAbsolute), + 'extension' => $file->extension(), + 'filename' => $file->filename(), + 'id' => $file->id(), + 'icon' => $file->panelIcon($image), + 'image' => $image, + 'info' => $file->toString($this->info ?? false), + 'link' => $file->panelUrl(true), + 'mime' => $file->mime(), + 'parent' => $file->parent()->panelPath(), + 'text' => $file->toString($this->text), + 'url' => $file->url(), + ]; + } + + return $data; + }, + 'total' => function () { + return $this->files->pagination()->total(); + }, + 'errors' => function () { + $errors = []; + + if ($this->validateMax() === false) { + $errors['max'] = I18n::template('error.section.files.max.' . I18n::form($this->max), [ + 'max' => $this->max, + 'section' => $this->headline + ]); + } + + if ($this->validateMin() === false) { + $errors['min'] = I18n::template('error.section.files.min.' . I18n::form($this->min), [ + 'min' => $this->min, + 'section' => $this->headline + ]); + } + + if (empty($errors) === true) { + return []; + } + + return [ + $this->name => [ + 'label' => $this->headline, + 'message' => $errors, + ] + ]; + }, + 'link' => function () { + $modelLink = $this->model->panelUrl(true); + $parentLink = $this->parent->panelUrl(true); + + if ($modelLink !== $parentLink) { + return $parentLink; + } + }, + 'pagination' => function () { + return $this->pagination(); + }, + 'sortable' => function () { + if ($this->sortable === false) { + return false; + } + + if ($this->sortBy !== null) { + return false; + } + + if ($this->flip === 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; + + if ($this->max && $total === $this->max - 1) { + $multiple = false; + } else { + $multiple = true; + } + + return [ + 'accept' => $this->accept, + 'multiple' => $multiple, + 'max' => $max, + 'api' => $this->parent->apiUrl(true) . '/files', + 'attributes' => array_filter([ + 'template' => $this->template + ]) + ]; + } + ], + 'toArray' => function () { + return [ + 'data' => $this->data, + 'errors' => $this->errors, + 'options' => [ + 'accept' => $this->accept, + '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 + ]; + } +]; diff --git a/kirby/config/sections/info.php b/kirby/config/sections/info.php new file mode 100755 index 0000000..254a76b --- /dev/null +++ b/kirby/config/sections/info.php @@ -0,0 +1,35 @@ + [ + '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()->toString($this->text); + $text = $this->kirby()->kirbytext($text); + return $text; + } + }, + ], + 'toArray' => function () { + return [ + 'options' => [ + 'headline' => $this->headline, + 'text' => $this->text, + 'theme' => $this->theme + ] + ]; + } +]; diff --git a/kirby/config/sections/mixins/empty.php b/kirby/config/sections/mixins/empty.php new file mode 100755 index 0000000..1c58194 --- /dev/null +++ b/kirby/config/sections/mixins/empty.php @@ -0,0 +1,21 @@ + [ + /** + * 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()->toString($this->empty); + } + } + ] +]; diff --git a/kirby/config/sections/mixins/headline.php b/kirby/config/sections/mixins/headline.php new file mode 100755 index 0000000..f4bb7e1 --- /dev/null +++ b/kirby/config/sections/mixins/headline.php @@ -0,0 +1,23 @@ + [ + /** + * 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); + } + + return ucfirst($this->name); + } + ] +]; diff --git a/kirby/config/sections/mixins/help.php b/kirby/config/sections/mixins/help.php new file mode 100755 index 0000000..80f42ee --- /dev/null +++ b/kirby/config/sections/mixins/help.php @@ -0,0 +1,23 @@ + [ + /** + * Sets the help text + */ + 'help' => function ($help = null) { + return I18n::translate($help, $help); + } + ], + 'computed' => [ + 'help' => function () { + if ($this->help) { + $help = $this->model()->toString($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 new file mode 100755 index 0000000..4a7a621 --- /dev/null +++ b/kirby/config/sections/mixins/layout.php @@ -0,0 +1,12 @@ + [ + /** + * Section layout. Available layout methods: `list`, `cards`. + */ + 'layout' => function (string $layout = 'list') { + return $layout === 'cards' ? 'cards' : 'list'; + } + ] +]; diff --git a/kirby/config/sections/mixins/max.php b/kirby/config/sections/mixins/max.php new file mode 100755 index 0000000..5ce303c --- /dev/null +++ b/kirby/config/sections/mixins/max.php @@ -0,0 +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; + } + + return false; + }, + 'validateMax' => function () { + if ($this->max && $this->total > $this->max) { + return false; + } + + return true; + } + ] +]; diff --git a/kirby/config/sections/mixins/min.php b/kirby/config/sections/mixins/min.php new file mode 100755 index 0000000..bfc495d --- /dev/null +++ b/kirby/config/sections/mixins/min.php @@ -0,0 +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; + } + + return true; + } + ] +]; diff --git a/kirby/config/sections/mixins/pagination.php b/kirby/config/sections/mixins/pagination.php new file mode 100755 index 0000000..8bf3dee --- /dev/null +++ b/kirby/config/sections/mixins/pagination.php @@ -0,0 +1,36 @@ + [ + /** + * 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 + ]); + + 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 new file mode 100755 index 0000000..3534acf --- /dev/null +++ b/kirby/config/sections/mixins/parent.php @@ -0,0 +1,43 @@ + [ + /** + * 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 (!$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 ($parent === null) { + return $this->model; + } + + return $parent; + } + ] +]; diff --git a/kirby/config/sections/pages.php b/kirby/config/sections/pages.php new file mode 100755 index 0000000..9fd99e3 --- /dev/null +++ b/kirby/config/sections/pages.php @@ -0,0 +1,296 @@ + [ + 'empty', + 'headline', + 'help', + 'layout', + 'min', + 'max', + 'pagination', + 'parent' + ], + 'props' => [ + /** + * Optional array of templates that should only be allowed to add. + */ + 'create' => function ($add = null) { + return $add; + }, + /** + * 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 (string $info = null) { + return $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'; + } + + 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 (string $text = '{{ page.title }}') { + return $text; + } + ], + 'computed' => [ + 'parent' => function () { + return $this->parentModel(); + }, + '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) { + + // 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) { + unset($pages->data[$id]); + continue; + } + } + + // sort + if ($this->sortBy) { + $pages = $pages->sortBy(...$pages::sortArgs($this->sortBy)); + } + + // flip + if ($this->flip === true) { + $pages = $pages->flip(); + } + + // pagination + $pages = $pages->paginate([ + 'page' => $this->page, + 'limit' => $this->limit + ]); + + return $pages; + }, + 'total' => function () { + return $this->pages->pagination()->total(); + }, + 'data' => function () { + $data = []; + + foreach ($this->pages as $item) { + $permissions = $item->permissions(); + $image = $item->panelImage($this->image); + + $data[] = [ + 'id' => $item->id(), + 'dragText' => $item->dragText(), + 'text' => $item->toString($this->text), + 'info' => $item->toString($this->info ?? false), + 'parent' => $item->parentId(), + 'icon' => $item->panelIcon($image), + 'image' => $image, + 'link' => $item->panelUrl(true), + 'status' => $item->status(), + 'permissions' => [ + 'sort' => $permissions->can('sort'), + 'changeStatus' => $permissions->can('changeStatus') + ] + ]; + } + + return $data; + }, + 'errors' => function () { + $errors = []; + + if ($this->validateMax() === false) { + $errors['max'] = I18n::template('error.section.pages.max.' . I18n::form($this->max), [ + 'max' => $this->max, + 'section' => $this->headline + ]); + } + + if ($this->validateMin() === false) { + $errors['min'] = I18n::template('error.section.pages.min.' . I18n::form($this->min), [ + 'min' => $this->min, + 'section' => $this->headline + ]); + } + + if (empty($errors) === true) { + return []; + } + + return [ + $this->name => [ + 'label' => $this->headline, + 'message' => $errors, + ] + ]; + }, + 'add' => function () { + if ($this->create === false) { + return false; + } + + if (in_array($this->status, ['draft', 'all']) === false) { + return false; + } + + if ($this->isFull() === true) { + return false; + } + + return true; + }, + 'link' => function () { + $modelLink = $this->model->panelUrl(true); + $parentLink = $this->parent->panelUrl(true); + + if ($modelLink !== $parentLink) { + return $parentLink; + } + }, + 'pagination' => function () { + return $this->pagination(); + }, + 'sortable' => function () { + if (in_array($this->status, ['listed', 'published', 'all']) === false) { + return false; + } + + if ($this->sortable === false) { + return false; + } + + if ($this->sortBy !== null) { + return false; + } + + if ($this->flip === true) { + return false; + } + + return true; + } + ], + 'methods' => [ + 'blueprints' => function () { + $blueprints = []; + $templates = empty($this->create) === false ? A::wrap($this->create) : $this->templates; + + if (empty($templates) === true) { + $templates = $this->kirby()->blueprints(); + } + + // 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, + ]; + } +]; diff --git a/kirby/config/setup.php b/kirby/config/setup.php new file mode 100755 index 0000000..acbc2df --- /dev/null +++ b/kirby/config/setup.php @@ -0,0 +1,41 @@ + [ + '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, + ]); + } + ], + + /** + * 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()); + } + + 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); + } + ], + + /** + * 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; + } + + 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 + ]); + }; + + $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); + } + + // render KirbyText in caption + if ($tag->caption) { + $tag->caption = [$tag->kirby()->kirbytext($tag->caption, [], true)]; + } + + 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); + } + + 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 + ]); + } + ], + + /** + * Twitter + */ + 'twitter' => [ + 'attr' => [ + 'class', + 'rel', + 'target', + 'text', + 'title' + ], + 'html' => function ($tag) { + + // get and sanitize the username + $username = str_replace('@', '', $tag->value); + + // build the profile url + $url = 'https://twitter.com/' . $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, + ]); + } + ], + + /** + * Video + */ + 'video' => [ + 'attr' => [ + 'class', + 'caption', + 'height', + 'width' + ], + 'html' => function ($tag) { + $video = Html::video( + $tag->value, + $tag->kirby()->option('kirbytext.video.options', []), + [ + 'height' => $tag->height ?? $tag->kirby()->option('kirbytext.video.height'), + 'width' => $tag->width ?? $tag->kirby()->option('kirbytext.video.width'), + ] + ); + + return Html::figure([$video], $tag->caption, [ + 'class' => $tag->class ?? $tag->kirby()->option('kirbytext.video.class', 'video'), + ]); + } + ], + +]; diff --git a/kirby/config/urls.php b/kirby/config/urls.php new file mode 100755 index 0000000..1bfe0fd --- /dev/null +++ b/kirby/config/urls.php @@ -0,0 +1,33 @@ + function () { + return Url::index(); + }, + 'base' => function (array $urls) { + return rtrim($urls['index'], '/'); + }, + 'current' => function (array $urls) { + $path = trim($this->path(), '/'); + + if (empty($path) === true) { + return $urls['index']; + } else { + return $urls['base'] . '/' . $path; + } + }, + 'assets' => function (array $urls) { + return $urls['base'] . '/assets'; + }, + 'api' => function (array $urls) { + return $urls['base'] . '/' . ($this->options['api']['slug'] ?? 'api'); + }, + 'media' => function (array $urls) { + return $urls['base'] . '/media'; + }, + 'panel' => function (array $urls) { + return $urls['base'] . '/' . ($this->options['panel']['slug'] ?? 'panel'); + } +]; diff --git a/kirby/dependencies/parsedown-extra/ParsedownExtra.php b/kirby/dependencies/parsedown-extra/ParsedownExtra.php new file mode 100755 index 0000000..9e1a748 --- /dev/null +++ b/kirby/dependencies/parsedown-extra/ParsedownExtra.php @@ -0,0 +1,624 @@ +BlockTypes[':'] []= 'DefinitionList'; + $this->BlockTypes['*'] []= 'Abbreviation'; + + # identify footnote definitions before reference definitions + array_unshift($this->BlockTypes['['], 'Footnote'); + + # identify footnote markers before before links + array_unshift($this->InlineTypes['['], 'FootnoteMarker'); + } + + # + # ~ + + public function text($text) + { + $Elements = $this->textElements($text); + + # convert to markup + $markup = $this->elements($Elements); + + # trim line breaks + $markup = trim($markup, "\n"); + + # merge consecutive dl elements + + $markup = preg_replace('/<\/dl>\s+

\s+/', '', $markup); + + # add footnotes + + if (isset($this->DefinitionData['Footnote'])) { + $Element = $this->buildFootnoteElement(); + + $markup .= "\n" . $this->element($Element); + } + + return $markup; + } + + # + # Blocks + # + + # + # Abbreviation + + protected function blockAbbreviation($Line) + { + if (preg_match('/^\*\[(.+?)\]:[ ]*(.+?)[ ]*$/', $Line['text'], $matches)) { + $this->DefinitionData['Abbreviation'][$matches[1]] = $matches[2]; + + $Block = array( + 'hidden' => true, + ); + + return $Block; + } + } + + # + # Footnote + + protected function blockFootnote($Line) + { + if (preg_match('/^\[\^(.+?)\]:[ ]?(.*)$/', $Line['text'], $matches)) { + $Block = array( + 'label' => $matches[1], + 'text' => $matches[2], + 'hidden' => true, + ); + + return $Block; + } + } + + protected function blockFootnoteContinue($Line, $Block) + { + if ($Line['text'][0] === '[' and preg_match('/^\[\^(.+?)\]:/', $Line['text'])) { + return; + } + + if (isset($Block['interrupted'])) { + if ($Line['indent'] >= 4) { + $Block['text'] .= "\n\n" . $Line['text']; + + return $Block; + } + } else { + $Block['text'] .= "\n" . $Line['text']; + + return $Block; + } + } + + protected function blockFootnoteComplete($Block) + { + $this->DefinitionData['Footnote'][$Block['label']] = array( + 'text' => $Block['text'], + 'count' => null, + 'number' => null, + ); + + return $Block; + } + + # + # Definition List + + protected function blockDefinitionList($Line, $Block) + { + if (! isset($Block) or $Block['type'] !== 'Paragraph') { + return; + } + + $Element = array( + 'name' => 'dl', + 'elements' => array(), + ); + + $terms = explode("\n", $Block['element']['handler']['argument']); + + foreach ($terms as $term) { + $Element['elements'] []= array( + 'name' => 'dt', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $term, + 'destination' => 'elements' + ), + ); + } + + $Block['element'] = $Element; + + $Block = $this->addDdElement($Line, $Block); + + return $Block; + } + + protected function blockDefinitionListContinue($Line, array $Block) + { + if ($Line['text'][0] === ':') { + $Block = $this->addDdElement($Line, $Block); + + return $Block; + } else { + if (isset($Block['interrupted']) and $Line['indent'] === 0) { + return; + } + + if (isset($Block['interrupted'])) { + $Block['dd']['handler']['function'] = 'textElements'; + $Block['dd']['handler']['argument'] .= "\n\n"; + + $Block['dd']['handler']['destination'] = 'elements'; + + unset($Block['interrupted']); + } + + $text = substr($Line['body'], min($Line['indent'], 4)); + + $Block['dd']['handler']['argument'] .= "\n" . $text; + + return $Block; + } + } + + # + # Header + + protected function blockHeader($Line) + { + $Block = parent::blockHeader($Line); + + if (preg_match('/[ #]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, PREG_OFFSET_CAPTURE)) { + $attributeString = $matches[1][0]; + + $Block['element']['attributes'] = $this->parseAttributeData($attributeString); + + $Block['element']['handler']['argument'] = substr($Block['element']['handler']['argument'], 0, $matches[0][1]); + } + + return $Block; + } + + # + # Markup + + protected function blockMarkup($Line) + { + if ($this->markupEscaped or $this->safeMode) { + return; + } + + if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) { + $element = strtolower($matches[1]); + + if (in_array($element, $this->textLevelElements)) { + return; + } + + $Block = array( + 'name' => $matches[1], + 'depth' => 0, + 'element' => array( + 'rawHtml' => $Line['text'], + 'autobreak' => true, + ), + ); + + $length = strlen($matches[0]); + $remainder = substr($Line['text'], $length); + + if (trim($remainder) === '') { + if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) { + $Block['closed'] = true; + $Block['void'] = true; + } + } else { + if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) { + return; + } + if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder)) { + $Block['closed'] = true; + } + } + + return $Block; + } + } + + protected function blockMarkupContinue($Line, array $Block) + { + if (isset($Block['closed'])) { + return; + } + + if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) { # open + $Block['depth'] ++; + } + + if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) { # close + if ($Block['depth'] > 0) { + $Block['depth'] --; + } else { + $Block['closed'] = true; + } + } + + if (isset($Block['interrupted'])) { + $Block['element']['rawHtml'] .= "\n"; + unset($Block['interrupted']); + } + + $Block['element']['rawHtml'] .= "\n".$Line['body']; + + return $Block; + } + + protected function blockMarkupComplete($Block) + { + if (! isset($Block['void'])) { + $Block['element']['rawHtml'] = $this->processTag($Block['element']['rawHtml']); + } + + return $Block; + } + + # + # Setext + + protected function blockSetextHeader($Line, array $Block = null) + { + $Block = parent::blockSetextHeader($Line, $Block); + + if (preg_match('/[ ]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, PREG_OFFSET_CAPTURE)) { + $attributeString = $matches[1][0]; + + $Block['element']['attributes'] = $this->parseAttributeData($attributeString); + + $Block['element']['handler']['argument'] = substr($Block['element']['handler']['argument'], 0, $matches[0][1]); + } + + return $Block; + } + + # + # Inline Elements + # + + # + # Footnote Marker + + protected function inlineFootnoteMarker($Excerpt) + { + if (preg_match('/^\[\^(.+?)\]/', $Excerpt['text'], $matches)) { + $name = $matches[1]; + + if (! isset($this->DefinitionData['Footnote'][$name])) { + return; + } + + $this->DefinitionData['Footnote'][$name]['count'] ++; + + if (! isset($this->DefinitionData['Footnote'][$name]['number'])) { + $this->DefinitionData['Footnote'][$name]['number'] = ++ $this->footnoteCount; # » & + } + + $Element = array( + 'name' => 'sup', + 'attributes' => array('id' => 'fnref'.$this->DefinitionData['Footnote'][$name]['count'].':'.$name), + 'element' => array( + 'name' => 'a', + 'attributes' => array('href' => '#fn:'.$name, 'class' => 'footnote-ref'), + 'text' => $this->DefinitionData['Footnote'][$name]['number'], + ), + ); + + return array( + 'extent' => strlen($matches[0]), + 'element' => $Element, + ); + } + } + + private $footnoteCount = 0; + + # + # Link + + protected function inlineLink($Excerpt) + { + $Link = parent::inlineLink($Excerpt); + + $remainder = substr($Excerpt['text'], $Link['extent']); + + if (preg_match('/^[ ]*{('.$this->regexAttribute.'+)}/', $remainder, $matches)) { + $Link['element']['attributes'] += $this->parseAttributeData($matches[1]); + + $Link['extent'] += strlen($matches[0]); + } + + return $Link; + } + + # + # ~ + # + + private $currentAbreviation; + private $currentMeaning; + + protected function insertAbreviation(array $Element) + { + if (isset($Element['text'])) { + $Element['elements'] = self::pregReplaceElements( + '/\b'.preg_quote($this->currentAbreviation, '/').'\b/', + array( + array( + 'name' => 'abbr', + 'attributes' => array( + 'title' => $this->currentMeaning, + ), + 'text' => $this->currentAbreviation, + ) + ), + $Element['text'] + ); + + unset($Element['text']); + } + + return $Element; + } + + protected function inlineText($text) + { + $Inline = parent::inlineText($text); + + if (isset($this->DefinitionData['Abbreviation'])) { + foreach ($this->DefinitionData['Abbreviation'] as $abbreviation => $meaning) { + $this->currentAbreviation = $abbreviation; + $this->currentMeaning = $meaning; + + $Inline['element'] = $this->elementApplyRecursiveDepthFirst( + array($this, 'insertAbreviation'), + $Inline['element'] + ); + } + } + + return $Inline; + } + + # + # Util Methods + # + + protected function addDdElement(array $Line, array $Block) + { + $text = substr($Line['text'], 1); + $text = trim($text); + + unset($Block['dd']); + + $Block['dd'] = array( + 'name' => 'dd', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $text, + 'destination' => 'elements' + ), + ); + + if (isset($Block['interrupted'])) { + $Block['dd']['handler']['function'] = 'textElements'; + + unset($Block['interrupted']); + } + + $Block['element']['elements'] []= & $Block['dd']; + + return $Block; + } + + protected function buildFootnoteElement() + { + $Element = array( + 'name' => 'div', + 'attributes' => array('class' => 'footnotes'), + 'elements' => array( + array('name' => 'hr'), + array( + 'name' => 'ol', + 'elements' => array(), + ), + ), + ); + + uasort($this->DefinitionData['Footnote'], 'self::sortFootnotes'); + + foreach ($this->DefinitionData['Footnote'] as $definitionId => $DefinitionData) { + if (! isset($DefinitionData['number'])) { + continue; + } + + $text = $DefinitionData['text']; + + $textElements = parent::textElements($text); + + $numbers = range(1, $DefinitionData['count']); + + $backLinkElements = array(); + + foreach ($numbers as $number) { + $backLinkElements[] = array('text' => ' '); + $backLinkElements[] = array( + 'name' => 'a', + 'attributes' => array( + 'href' => "#fnref$number:$definitionId", + 'rev' => 'footnote', + 'class' => 'footnote-backref', + ), + 'rawHtml' => '↩', + 'allowRawHtmlInSafeMode' => true, + 'autobreak' => false, + ); + } + + unset($backLinkElements[0]); + + $n = count($textElements) -1; + + if ($textElements[$n]['name'] === 'p') { + $backLinkElements = array_merge( + array( + array( + 'rawHtml' => ' ', + 'allowRawHtmlInSafeMode' => true, + ), + ), + $backLinkElements + ); + + unset($textElements[$n]['name']); + + $textElements[$n] = array( + 'name' => 'p', + 'elements' => array_merge( + array($textElements[$n]), + $backLinkElements + ), + ); + } else { + $textElements[] = array( + 'name' => 'p', + 'elements' => $backLinkElements + ); + } + + $Element['elements'][1]['elements'] []= array( + 'name' => 'li', + 'attributes' => array('id' => 'fn:'.$definitionId), + 'elements' => array_merge( + $textElements + ), + ); + } + + return $Element; + } + + # ~ + + protected function parseAttributeData($attributeString) + { + $Data = array(); + + $attributes = preg_split('/[ ]+/', $attributeString, - 1, PREG_SPLIT_NO_EMPTY); + + foreach ($attributes as $attribute) { + if ($attribute[0] === '#') { + $Data['id'] = substr($attribute, 1); + } else { # "." + $classes []= substr($attribute, 1); + } + } + + if (isset($classes)) { + $Data['class'] = implode(' ', $classes); + } + + return $Data; + } + + # ~ + + protected function processTag($elementMarkup) # recursive + { + # http://stackoverflow.com/q/1148928/200145 + libxml_use_internal_errors(true); + + $DOMDocument = new DOMDocument; + + # http://stackoverflow.com/q/11309194/200145 + $elementMarkup = mb_convert_encoding($elementMarkup, 'HTML-ENTITIES', 'UTF-8'); + + # http://stackoverflow.com/q/4879946/200145 + $DOMDocument->loadHTML($elementMarkup); + $DOMDocument->removeChild($DOMDocument->doctype); + $DOMDocument->replaceChild($DOMDocument->firstChild->firstChild->firstChild, $DOMDocument->firstChild); + + $elementText = ''; + + if ($DOMDocument->documentElement->getAttribute('markdown') === '1') { + foreach ($DOMDocument->documentElement->childNodes as $Node) { + $elementText .= $DOMDocument->saveHTML($Node); + } + + $DOMDocument->documentElement->removeAttribute('markdown'); + + $elementText = "\n".$this->text($elementText)."\n"; + } else { + foreach ($DOMDocument->documentElement->childNodes as $Node) { + $nodeMarkup = $DOMDocument->saveHTML($Node); + + if ($Node instanceof DOMElement and ! in_array($Node->nodeName, $this->textLevelElements)) { + $elementText .= $this->processTag($nodeMarkup); + } else { + $elementText .= $nodeMarkup; + } + } + } + + # because we don't want for markup to get encoded + $DOMDocument->documentElement->nodeValue = 'placeholder\x1A'; + + $markup = $DOMDocument->saveHTML($DOMDocument->documentElement); + $markup = str_replace('placeholder\x1A', $elementText, $markup); + + return $markup; + } + + # ~ + + protected function sortFootnotes($A, $B) # callback + { + return $A['number'] - $B['number']; + } + + # + # Fields + # + + protected $regexAttribute = '(?:[#.][-\w]+[ ]*)'; +} diff --git a/kirby/dependencies/parsedown/Parsedown.php b/kirby/dependencies/parsedown/Parsedown.php new file mode 100755 index 0000000..6552c0c --- /dev/null +++ b/kirby/dependencies/parsedown/Parsedown.php @@ -0,0 +1,1822 @@ +textElements($text); + + # convert to markup + $markup = $this->elements($Elements); + + # trim line breaks + $markup = trim($markup, "\n"); + + return $markup; + } + + protected function textElements($text) + { + # make sure no definitions are set + $this->DefinitionData = array(); + + # standardize line breaks + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + # remove surrounding line breaks + $text = trim($text, "\n"); + + # split text into lines + $lines = explode("\n", $text); + + # iterate through lines to identify blocks + return $this->linesElements($lines); + } + + # + # Setters + # + + public function setBreaksEnabled($breaksEnabled) + { + $this->breaksEnabled = $breaksEnabled; + + return $this; + } + + protected $breaksEnabled; + + public function setMarkupEscaped($markupEscaped) + { + $this->markupEscaped = $markupEscaped; + + return $this; + } + + protected $markupEscaped; + + public function setUrlsLinked($urlsLinked) + { + $this->urlsLinked = $urlsLinked; + + return $this; + } + + protected $urlsLinked = true; + + public function setSafeMode($safeMode) + { + $this->safeMode = (bool) $safeMode; + + return $this; + } + + protected $safeMode; + + public function setStrictMode($strictMode) + { + $this->strictMode = (bool) $strictMode; + + return $this; + } + + protected $strictMode; + + protected $safeLinksWhitelist = array( + 'http://', + 'https://', + 'ftp://', + 'ftps://', + 'mailto:', + 'data:image/png;base64,', + 'data:image/gif;base64,', + 'data:image/jpeg;base64,', + 'irc:', + 'ircs:', + 'git:', + 'ssh:', + 'news:', + 'steam:', + ); + + # + # Lines + # + + protected $BlockTypes = array( + '#' => array('Header'), + '*' => array('Rule', 'List'), + '+' => array('List'), + '-' => array('SetextHeader', 'Table', 'Rule', 'List'), + '0' => array('List'), + '1' => array('List'), + '2' => array('List'), + '3' => array('List'), + '4' => array('List'), + '5' => array('List'), + '6' => array('List'), + '7' => array('List'), + '8' => array('List'), + '9' => array('List'), + ':' => array('Table'), + '<' => array('Comment', 'Markup'), + '=' => array('SetextHeader'), + '>' => array('Quote'), + '[' => array('Reference'), + '_' => array('Rule'), + '`' => array('FencedCode'), + '|' => array('Table'), + '~' => array('FencedCode'), + ); + + # ~ + + protected $unmarkedBlockTypes = array( + 'Code', + ); + + # + # Blocks + # + + protected function lines(array $lines) + { + return $this->elements($this->linesElements($lines)); + } + + protected function linesElements(array $lines) + { + $Elements = array(); + $CurrentBlock = null; + + foreach ($lines as $line) { + if (chop($line) === '') { + if (isset($CurrentBlock)) { + $CurrentBlock['interrupted'] = ( + isset($CurrentBlock['interrupted']) + ? $CurrentBlock['interrupted'] + 1 : 1 + ); + } + + continue; + } + + while (($beforeTab = strstr($line, "\t", true)) !== false) { + $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4; + + $line = $beforeTab + . str_repeat(' ', $shortage) + . substr($line, strlen($beforeTab) + 1) + ; + } + + $indent = strspn($line, ' '); + + $text = $indent > 0 ? substr($line, $indent) : $line; + + # ~ + + $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); + + # ~ + + if (isset($CurrentBlock['continuable'])) { + $methodName = 'block' . $CurrentBlock['type'] . 'Continue'; + $Block = $this->$methodName($Line, $CurrentBlock); + + if (isset($Block)) { + $CurrentBlock = $Block; + + continue; + } else { + if ($this->isBlockCompletable($CurrentBlock['type'])) { + $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; + $CurrentBlock = $this->$methodName($CurrentBlock); + } + } + } + + # ~ + + $marker = $text[0]; + + # ~ + + $blockTypes = $this->unmarkedBlockTypes; + + if (isset($this->BlockTypes[$marker])) { + foreach ($this->BlockTypes[$marker] as $blockType) { + $blockTypes []= $blockType; + } + } + + # + # ~ + + foreach ($blockTypes as $blockType) { + $Block = $this->{"block$blockType"}($Line, $CurrentBlock); + + if (isset($Block)) { + $Block['type'] = $blockType; + + if (! isset($Block['identified'])) { + if (isset($CurrentBlock)) { + $Elements[] = $this->extractElement($CurrentBlock); + } + + $Block['identified'] = true; + } + + if ($this->isBlockContinuable($blockType)) { + $Block['continuable'] = true; + } + + $CurrentBlock = $Block; + + continue 2; + } + } + + # ~ + + if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph') { + $Block = $this->paragraphContinue($Line, $CurrentBlock); + } + + if (isset($Block)) { + $CurrentBlock = $Block; + } else { + if (isset($CurrentBlock)) { + $Elements[] = $this->extractElement($CurrentBlock); + } + + $CurrentBlock = $this->paragraph($Line); + + $CurrentBlock['identified'] = true; + } + } + + # ~ + + if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) { + $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; + $CurrentBlock = $this->$methodName($CurrentBlock); + } + + # ~ + + if (isset($CurrentBlock)) { + $Elements[] = $this->extractElement($CurrentBlock); + } + + # ~ + + return $Elements; + } + + protected function extractElement(array $Component) + { + if (! isset($Component['element'])) { + if (isset($Component['markup'])) { + $Component['element'] = array('rawHtml' => $Component['markup']); + } elseif (isset($Component['hidden'])) { + $Component['element'] = array(); + } + } + + return $Component['element']; + } + + protected function isBlockContinuable($Type) + { + return method_exists($this, 'block' . $Type . 'Continue'); + } + + protected function isBlockCompletable($Type) + { + return method_exists($this, 'block' . $Type . 'Complete'); + } + + # + # Code + + protected function blockCode($Line, $Block = null) + { + if (isset($Block) and $Block['type'] === 'Paragraph' and ! isset($Block['interrupted'])) { + return; + } + + if ($Line['indent'] >= 4) { + $text = substr($Line['body'], 4); + + $Block = array( + 'element' => array( + 'name' => 'pre', + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ), + ); + + return $Block; + } + } + + protected function blockCodeContinue($Line, $Block) + { + if ($Line['indent'] >= 4) { + if (isset($Block['interrupted'])) { + $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); + + unset($Block['interrupted']); + } + + $Block['element']['element']['text'] .= "\n"; + + $text = substr($Line['body'], 4); + + $Block['element']['element']['text'] .= $text; + + return $Block; + } + } + + protected function blockCodeComplete($Block) + { + return $Block; + } + + # + # Comment + + protected function blockComment($Line) + { + if ($this->markupEscaped or $this->safeMode) { + return; + } + + if (strpos($Line['text'], '') !== false) { + $Block['closed'] = true; + } + + return $Block; + } + } + + protected function blockCommentContinue($Line, array $Block) + { + if (isset($Block['closed'])) { + return; + } + + $Block['element']['rawHtml'] .= "\n" . $Line['body']; + + if (strpos($Line['text'], '-->') !== false) { + $Block['closed'] = true; + } + + return $Block; + } + + # + # Fenced Code + + protected function blockFencedCode($Line) + { + $marker = $Line['text'][0]; + + $openerLength = strspn($Line['text'], $marker); + + if ($openerLength < 3) { + return; + } + + $infostring = trim(substr($Line['text'], $openerLength), "\t "); + + if (strpos($infostring, '`') !== false) { + return; + } + + $Element = array( + 'name' => 'code', + 'text' => '', + ); + + if ($infostring !== '') { + /** + * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes + * Every HTML element may have a class attribute specified. + * The attribute, if specified, must have a value that is a set + * of space-separated tokens representing the various classes + * that the element belongs to. + * [...] + * The space characters, for the purposes of this specification, + * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), + * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and + * U+000D CARRIAGE RETURN (CR). + */ + $language = substr($infostring, 0, strcspn($infostring, " \t\n\f\r")); + + $Element['attributes'] = array('class' => "language-$language"); + } + + $Block = array( + 'char' => $marker, + 'openerLength' => $openerLength, + 'element' => array( + 'name' => 'pre', + 'element' => $Element, + ), + ); + + return $Block; + } + + protected function blockFencedCodeContinue($Line, $Block) + { + if (isset($Block['complete'])) { + return; + } + + if (isset($Block['interrupted'])) { + $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); + + unset($Block['interrupted']); + } + + if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength'] + and chop(substr($Line['text'], $len), ' ') === '' + ) { + $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1); + + $Block['complete'] = true; + + return $Block; + } + + $Block['element']['element']['text'] .= "\n" . $Line['body']; + + return $Block; + } + + protected function blockFencedCodeComplete($Block) + { + return $Block; + } + + # + # Header + + protected function blockHeader($Line) + { + $level = strspn($Line['text'], '#'); + + if ($level > 6) { + return; + } + + $text = trim($Line['text'], '#'); + + if ($this->strictMode and isset($text[0]) and $text[0] !== ' ') { + return; + } + + $text = trim($text, ' '); + + $Block = array( + 'element' => array( + 'name' => 'h' . min(6, $level), + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $text, + 'destination' => 'elements', + ) + ), + ); + + return $Block; + } + + # + # List + + protected function blockList($Line, array $CurrentBlock = null) + { + list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\)]'); + + if (preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches)) { + $contentIndent = strlen($matches[2]); + + if ($contentIndent >= 5) { + $contentIndent -= 1; + $matches[1] = substr($matches[1], 0, -$contentIndent); + $matches[3] = str_repeat(' ', $contentIndent) . $matches[3]; + } elseif ($contentIndent === 0) { + $matches[1] .= ' '; + } + + $markerWithoutWhitespace = strstr($matches[1], ' ', true); + + $Block = array( + 'indent' => $Line['indent'], + 'pattern' => $pattern, + 'data' => array( + 'type' => $name, + 'marker' => $matches[1], + 'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)), + ), + 'element' => array( + 'name' => $name, + 'elements' => array(), + ), + ); + $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/'); + + if ($name === 'ol') { + $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0'; + + if ($listStart !== '1') { + if ( + isset($CurrentBlock) + and $CurrentBlock['type'] === 'Paragraph' + and ! isset($CurrentBlock['interrupted']) + ) { + return; + } + + $Block['element']['attributes'] = array('start' => $listStart); + } + } + + $Block['li'] = array( + 'name' => 'li', + 'handler' => array( + 'function' => 'li', + 'argument' => !empty($matches[3]) ? array($matches[3]) : array(), + 'destination' => 'elements' + ) + ); + + $Block['element']['elements'] []= & $Block['li']; + + return $Block; + } + } + + protected function blockListContinue($Line, array $Block) + { + if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument'])) { + return null; + } + + $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker'])); + + if ($Line['indent'] < $requiredIndent + and ( + ( + $Block['data']['type'] === 'ol' + and preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) + ) or ( + $Block['data']['type'] === 'ul' + and preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) + ) + ) + ) { + if (isset($Block['interrupted'])) { + $Block['li']['handler']['argument'] []= ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + unset($Block['li']); + + $text = isset($matches[1]) ? $matches[1] : ''; + + $Block['indent'] = $Line['indent']; + + $Block['li'] = array( + 'name' => 'li', + 'handler' => array( + 'function' => 'li', + 'argument' => array($text), + 'destination' => 'elements' + ) + ); + + $Block['element']['elements'] []= & $Block['li']; + + return $Block; + } elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line)) { + return null; + } + + if ($Line['text'][0] === '[' and $this->blockReference($Line)) { + return $Block; + } + + if ($Line['indent'] >= $requiredIndent) { + if (isset($Block['interrupted'])) { + $Block['li']['handler']['argument'] []= ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + $text = substr($Line['body'], $requiredIndent); + + $Block['li']['handler']['argument'] []= $text; + + return $Block; + } + + if (! isset($Block['interrupted'])) { + $text = preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']); + + $Block['li']['handler']['argument'] []= $text; + + return $Block; + } + } + + protected function blockListComplete(array $Block) + { + if (isset($Block['loose'])) { + foreach ($Block['element']['elements'] as &$li) { + if (end($li['handler']['argument']) !== '') { + $li['handler']['argument'] []= ''; + } + } + } + + return $Block; + } + + # + # Quote + + protected function blockQuote($Line) + { + if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) { + $Block = array( + 'element' => array( + 'name' => 'blockquote', + 'handler' => array( + 'function' => 'linesElements', + 'argument' => (array) $matches[1], + 'destination' => 'elements', + ) + ), + ); + + return $Block; + } + } + + protected function blockQuoteContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) { + return; + } + + if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) { + $Block['element']['handler']['argument'] []= $matches[1]; + + return $Block; + } + + if (! isset($Block['interrupted'])) { + $Block['element']['handler']['argument'] []= $Line['text']; + + return $Block; + } + } + + # + # Rule + + protected function blockRule($Line) + { + $marker = $Line['text'][0]; + + if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '') { + $Block = array( + 'element' => array( + 'name' => 'hr', + ), + ); + + return $Block; + } + } + + # + # Setext + + protected function blockSetextHeader($Line, array $Block = null) + { + if (! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) { + return; + } + + if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '') { + $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; + + return $Block; + } + } + + # + # Markup + + protected function blockMarkup($Line) + { + if ($this->markupEscaped or $this->safeMode) { + return; + } + + if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+(\/)?>/', $Line['text'], $matches)) { + $element = strtolower($matches[1]); + + if (in_array($element, $this->textLevelElements)) { + return; + } + + $Block = array( + 'name' => $matches[1], + 'element' => array( + 'rawHtml' => $Line['text'], + 'autobreak' => true, + ), + ); + + return $Block; + } + } + + protected function blockMarkupContinue($Line, array $Block) + { + if (isset($Block['closed']) or isset($Block['interrupted'])) { + return; + } + + $Block['element']['rawHtml'] .= "\n" . $Line['body']; + + return $Block; + } + + # + # Reference + + protected function blockReference($Line) + { + if (strpos($Line['text'], ']') !== false + and preg_match('/^\[(.+?)\]:[ ]*+?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches) + ) { + $id = strtolower($matches[1]); + + $Data = array( + 'url' => $matches[2], + 'title' => isset($matches[3]) ? $matches[3] : null, + ); + + $this->DefinitionData['Reference'][$id] = $Data; + + $Block = array( + 'element' => array(), + ); + + return $Block; + } + } + + # + # Table + + protected function blockTable($Line, array $Block = null) + { + if (! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) { + return; + } + + if ( + strpos($Block['element']['handler']['argument'], '|') === false + and strpos($Line['text'], '|') === false + and strpos($Line['text'], ':') === false + or strpos($Block['element']['handler']['argument'], "\n") !== false + ) { + return; + } + + if (chop($Line['text'], ' -:|') !== '') { + return; + } + + $alignments = array(); + + $divider = $Line['text']; + + $divider = trim($divider); + $divider = trim($divider, '|'); + + $dividerCells = explode('|', $divider); + + foreach ($dividerCells as $dividerCell) { + $dividerCell = trim($dividerCell); + + if ($dividerCell === '') { + return; + } + + $alignment = null; + + if ($dividerCell[0] === ':') { + $alignment = 'left'; + } + + if (substr($dividerCell, - 1) === ':') { + $alignment = $alignment === 'left' ? 'center' : 'right'; + } + + $alignments []= $alignment; + } + + # ~ + + $HeaderElements = array(); + + $header = $Block['element']['handler']['argument']; + + $header = trim($header); + $header = trim($header, '|'); + + $headerCells = explode('|', $header); + + if (count($headerCells) !== count($alignments)) { + return; + } + + foreach ($headerCells as $index => $headerCell) { + $headerCell = trim($headerCell); + + $HeaderElement = array( + 'name' => 'th', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $headerCell, + 'destination' => 'elements', + ) + ); + + if (isset($alignments[$index])) { + $alignment = $alignments[$index]; + + $HeaderElement['attributes'] = array( + 'style' => "text-align: $alignment;", + ); + } + + $HeaderElements []= $HeaderElement; + } + + # ~ + + $Block = array( + 'alignments' => $alignments, + 'identified' => true, + 'element' => array( + 'name' => 'table', + 'elements' => array(), + ), + ); + + $Block['element']['elements'] []= array( + 'name' => 'thead', + ); + + $Block['element']['elements'] []= array( + 'name' => 'tbody', + 'elements' => array(), + ); + + $Block['element']['elements'][0]['elements'] []= array( + 'name' => 'tr', + 'elements' => $HeaderElements, + ); + + return $Block; + } + + protected function blockTableContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) { + return; + } + + if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|')) { + $Elements = array(); + + $row = $Line['text']; + + $row = trim($row); + $row = trim($row, '|'); + + preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches); + + $cells = array_slice($matches[0], 0, count($Block['alignments'])); + + foreach ($cells as $index => $cell) { + $cell = trim($cell); + + $Element = array( + 'name' => 'td', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $cell, + 'destination' => 'elements', + ) + ); + + if (isset($Block['alignments'][$index])) { + $Element['attributes'] = array( + 'style' => 'text-align: ' . $Block['alignments'][$index] . ';', + ); + } + + $Elements []= $Element; + } + + $Element = array( + 'name' => 'tr', + 'elements' => $Elements, + ); + + $Block['element']['elements'][1]['elements'] []= $Element; + + return $Block; + } + } + + # + # ~ + # + + protected function paragraph($Line) + { + return array( + 'type' => 'Paragraph', + 'element' => array( + 'name' => 'p', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $Line['text'], + 'destination' => 'elements', + ), + ), + ); + } + + protected function paragraphContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) { + return; + } + + $Block['element']['handler']['argument'] .= "\n".$Line['text']; + + return $Block; + } + + # + # Inline Elements + # + + protected $InlineTypes = array( + '!' => array('Image'), + '&' => array('SpecialCharacter'), + '*' => array('Emphasis'), + ':' => array('Url'), + '<' => array('UrlTag', 'EmailTag', 'Markup'), + '[' => array('Link'), + '_' => array('Emphasis'), + '`' => array('Code'), + '~' => array('Strikethrough'), + '\\' => array('EscapeSequence'), + ); + + # ~ + + protected $inlineMarkerList = '!*_&[:<`~\\'; + + # + # ~ + # + + public function line($text, $nonNestables = array()) + { + return $this->elements($this->lineElements($text, $nonNestables)); + } + + protected function lineElements($text, $nonNestables = array()) + { + $Elements = array(); + + $nonNestables = ( + empty($nonNestables) + ? array() + : array_combine($nonNestables, $nonNestables) + ); + + # $excerpt is based on the first occurrence of a marker + + while ($excerpt = strpbrk($text, $this->inlineMarkerList)) { + $marker = $excerpt[0]; + + $markerPosition = strlen($text) - strlen($excerpt); + + $Excerpt = array('text' => $excerpt, 'context' => $text); + + foreach ($this->InlineTypes[$marker] as $inlineType) { + # check to see if the current inline type is nestable in the current context + + if (isset($nonNestables[$inlineType])) { + continue; + } + + $Inline = $this->{"inline$inlineType"}($Excerpt); + + if (! isset($Inline)) { + continue; + } + + # makes sure that the inline belongs to "our" marker + + if (isset($Inline['position']) and $Inline['position'] > $markerPosition) { + continue; + } + + # sets a default inline position + + if (! isset($Inline['position'])) { + $Inline['position'] = $markerPosition; + } + + # cause the new element to 'inherit' our non nestables + + + $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables']) + ? array_merge($Inline['element']['nonNestables'], $nonNestables) + : $nonNestables + ; + + # the text that comes before the inline + $unmarkedText = substr($text, 0, $Inline['position']); + + # compile the unmarked text + $InlineText = $this->inlineText($unmarkedText); + $Elements[] = $InlineText['element']; + + # compile the inline + $Elements[] = $this->extractElement($Inline); + + # remove the examined text + $text = substr($text, $Inline['position'] + $Inline['extent']); + + continue 2; + } + + # the marker does not belong to an inline + + $unmarkedText = substr($text, 0, $markerPosition + 1); + + $InlineText = $this->inlineText($unmarkedText); + $Elements[] = $InlineText['element']; + + $text = substr($text, $markerPosition + 1); + } + + $InlineText = $this->inlineText($text); + $Elements[] = $InlineText['element']; + + foreach ($Elements as &$Element) { + if (! isset($Element['autobreak'])) { + $Element['autobreak'] = false; + } + } + + return $Elements; + } + + # + # ~ + # + + protected function inlineText($text) + { + $Inline = array( + 'extent' => strlen($text), + 'element' => array(), + ); + + $Inline['element']['elements'] = self::pregReplaceElements( + $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/', + array( + array('name' => 'br'), + array('text' => "\n"), + ), + $text + ); + + return $Inline; + } + + protected function inlineCode($Excerpt) + { + $marker = $Excerpt['text'][0]; + + if (preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(? strlen($matches[0]), + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ); + } + } + + protected function inlineEmailTag($Excerpt) + { + $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'; + + $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@' + . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*'; + + if (strpos($Excerpt['text'], '>') !== false + and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches) + ) { + $url = $matches[1]; + + if (! isset($matches[2])) { + $url = "mailto:$url"; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $matches[1], + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + protected function inlineEmphasis($Excerpt) + { + if (! isset($Excerpt['text'][1])) { + return; + } + + $marker = $Excerpt['text'][0]; + + if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) { + $emphasis = 'strong'; + } elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) { + $emphasis = 'em'; + } else { + return; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => $emphasis, + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ) + ), + ); + } + + protected function inlineEscapeSequence($Excerpt) + { + if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) { + return array( + 'element' => array('rawHtml' => $Excerpt['text'][1]), + 'extent' => 2, + ); + } + } + + protected function inlineImage($Excerpt) + { + if (! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') { + return; + } + + $Excerpt['text']= substr($Excerpt['text'], 1); + + $Link = $this->inlineLink($Excerpt); + + if ($Link === null) { + return; + } + + $Inline = array( + 'extent' => $Link['extent'] + 1, + 'element' => array( + 'name' => 'img', + 'attributes' => array( + 'src' => $Link['element']['attributes']['href'], + 'alt' => $Link['element']['handler']['argument'], + ), + 'autobreak' => true, + ), + ); + + $Inline['element']['attributes'] += $Link['element']['attributes']; + + unset($Inline['element']['attributes']['href']); + + return $Inline; + } + + protected function inlineLink($Excerpt) + { + $Element = array( + 'name' => 'a', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => null, + 'destination' => 'elements', + ), + 'nonNestables' => array('Url', 'Link'), + 'attributes' => array( + 'href' => null, + 'title' => null, + ), + ); + + $extent = 0; + + $remainder = $Excerpt['text']; + + if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) { + $Element['handler']['argument'] = $matches[1]; + + $extent += strlen($matches[0]); + + $remainder = substr($remainder, $extent); + } else { + return; + } + + if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches)) { + $Element['attributes']['href'] = $matches[1]; + + if (isset($matches[2])) { + $Element['attributes']['title'] = substr($matches[2], 1, - 1); + } + + $extent += strlen($matches[0]); + } else { + if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) { + $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument']; + $definition = strtolower($definition); + + $extent += strlen($matches[0]); + } else { + $definition = strtolower($Element['handler']['argument']); + } + + if (! isset($this->DefinitionData['Reference'][$definition])) { + return; + } + + $Definition = $this->DefinitionData['Reference'][$definition]; + + $Element['attributes']['href'] = $Definition['url']; + $Element['attributes']['title'] = $Definition['title']; + } + + return array( + 'extent' => $extent, + 'element' => $Element, + ); + } + + protected function inlineMarkup($Excerpt) + { + if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) { + return; + } + + if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches)) { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches)) { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + } + + protected function inlineSpecialCharacter($Excerpt) + { + if ($Excerpt['text'][1] !== ' ' and strpos($Excerpt['text'], ';') !== false + and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches) + ) { + return array( + 'element' => array('rawHtml' => '&' . $matches[1] . ';'), + 'extent' => strlen($matches[0]), + ); + } + + return; + } + + protected function inlineStrikethrough($Excerpt) + { + if (! isset($Excerpt['text'][1])) { + return; + } + + if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) { + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'del', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ) + ), + ); + } + } + + protected function inlineUrl($Excerpt) + { + if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') { + return; + } + + if (strpos($Excerpt['context'], 'http') !== false + and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE) + ) { + $url = $matches[0][0]; + + $Inline = array( + 'extent' => strlen($matches[0][0]), + 'position' => $matches[0][1], + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + + return $Inline; + } + } + + protected function inlineUrlTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches)) { + $url = $matches[1]; + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + # ~ + + protected function unmarkedText($text) + { + $Inline = $this->inlineText($text); + return $this->element($Inline['element']); + } + + # + # Handlers + # + + protected function handle(array $Element) + { + if (isset($Element['handler'])) { + if (!isset($Element['nonNestables'])) { + $Element['nonNestables'] = array(); + } + + if (is_string($Element['handler'])) { + $function = $Element['handler']; + $argument = $Element['text']; + unset($Element['text']); + $destination = 'rawHtml'; + } else { + $function = $Element['handler']['function']; + $argument = $Element['handler']['argument']; + $destination = $Element['handler']['destination']; + } + + $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']); + + if ($destination === 'handler') { + $Element = $this->handle($Element); + } + + unset($Element['handler']); + } + + return $Element; + } + + protected function handleElementRecursive(array $Element) + { + return $this->elementApplyRecursive(array($this, 'handle'), $Element); + } + + protected function handleElementsRecursive(array $Elements) + { + return $this->elementsApplyRecursive(array($this, 'handle'), $Elements); + } + + protected function elementApplyRecursive($closure, array $Element) + { + $Element = call_user_func($closure, $Element); + + if (isset($Element['elements'])) { + $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']); + } elseif (isset($Element['element'])) { + $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']); + } + + return $Element; + } + + protected function elementApplyRecursiveDepthFirst($closure, array $Element) + { + if (isset($Element['elements'])) { + $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']); + } elseif (isset($Element['element'])) { + $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']); + } + + $Element = call_user_func($closure, $Element); + + return $Element; + } + + protected function elementsApplyRecursive($closure, array $Elements) + { + foreach ($Elements as &$Element) { + $Element = $this->elementApplyRecursive($closure, $Element); + } + + return $Elements; + } + + protected function elementsApplyRecursiveDepthFirst($closure, array $Elements) + { + foreach ($Elements as &$Element) { + $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element); + } + + return $Elements; + } + + protected function element(array $Element) + { + if ($this->safeMode) { + $Element = $this->sanitiseElement($Element); + } + + # identity map if element has no handler + $Element = $this->handle($Element); + + $hasName = isset($Element['name']); + + $markup = ''; + + if ($hasName) { + $markup .= '<' . $Element['name']; + + if (isset($Element['attributes'])) { + foreach ($Element['attributes'] as $name => $value) { + if ($value === null) { + continue; + } + + $markup .= " $name=\"".self::escape($value).'"'; + } + } + } + + $permitRawHtml = false; + + if (isset($Element['text'])) { + $text = $Element['text']; + } + // very strongly consider an alternative if you're writing an + // extension + elseif (isset($Element['rawHtml'])) { + $text = $Element['rawHtml']; + + $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; + $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; + } + + $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']); + + if ($hasContent) { + $markup .= $hasName ? '>' : ''; + + if (isset($Element['elements'])) { + $markup .= $this->elements($Element['elements']); + } elseif (isset($Element['element'])) { + $markup .= $this->element($Element['element']); + } else { + if (!$permitRawHtml) { + $markup .= self::escape($text, true); + } else { + $markup .= $text; + } + } + + $markup .= $hasName ? '' : ''; + } elseif ($hasName) { + $markup .= ' />'; + } + + return $markup; + } + + protected function elements(array $Elements) + { + $markup = ''; + + $autoBreak = true; + + foreach ($Elements as $Element) { + if (empty($Element)) { + continue; + } + + $autoBreakNext = ( + isset($Element['autobreak']) + ? $Element['autobreak'] : isset($Element['name']) + ); + // (autobreak === false) covers both sides of an element + $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext; + + $markup .= ($autoBreak ? "\n" : '') . $this->element($Element); + $autoBreak = $autoBreakNext; + } + + $markup .= $autoBreak ? "\n" : ''; + + return $markup; + } + + # ~ + + protected function li($lines) + { + $Elements = $this->linesElements($lines); + + if (! in_array('', $lines) + and isset($Elements[0]) and isset($Elements[0]['name']) + and $Elements[0]['name'] === 'p' + ) { + unset($Elements[0]['name']); + } + + return $Elements; + } + + # + # AST Convenience + # + + /** + * Replace occurrences $regexp with $Elements in $text. Return an array of + * elements representing the replacement. + */ + protected static function pregReplaceElements($regexp, $Elements, $text) + { + $newElements = array(); + + while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE)) { + $offset = $matches[0][1]; + $before = substr($text, 0, $offset); + $after = substr($text, $offset + strlen($matches[0][0])); + + $newElements[] = array('text' => $before); + + foreach ($Elements as $Element) { + $newElements[] = $Element; + } + + $text = $after; + } + + $newElements[] = array('text' => $text); + + return $newElements; + } + + # + # Deprecated Methods + # + + public function parse($text) + { + $markup = $this->text($text); + + return $markup; + } + + protected function sanitiseElement(array $Element) + { + static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; + static $safeUrlNameToAtt = array( + 'a' => 'href', + 'img' => 'src', + ); + + if (! isset($Element['name'])) { + unset($Element['attributes']); + return $Element; + } + + if (isset($safeUrlNameToAtt[$Element['name']])) { + $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); + } + + if (! empty($Element['attributes'])) { + foreach ($Element['attributes'] as $att => $val) { + # filter out badly parsed attribute + if (! preg_match($goodAttribute, $att)) { + unset($Element['attributes'][$att]); + } + # dump onevent attribute + elseif (self::striAtStart($att, 'on')) { + unset($Element['attributes'][$att]); + } + } + } + + return $Element; + } + + protected function filterUnsafeUrlInAttribute(array $Element, $attribute) + { + foreach ($this->safeLinksWhitelist as $scheme) { + if (self::striAtStart($Element['attributes'][$attribute], $scheme)) { + return $Element; + } + } + + $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); + + return $Element; + } + + # + # Static Methods + # + + protected static function escape($text, $allowQuotes = false) + { + return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); + } + + protected static function striAtStart($string, $needle) + { + $len = strlen($needle); + + if ($len > strlen($string)) { + return false; + } else { + return strtolower(substr($string, 0, $len)) === strtolower($needle); + } + } + + public static function instance($name = 'default') + { + if (isset(self::$instances[$name])) { + return self::$instances[$name]; + } + + $instance = new static(); + + self::$instances[$name] = $instance; + + return $instance; + } + + private static $instances = array(); + + # + # Fields + # + + protected $DefinitionData; + + # + # Read-Only + + protected $specialCharacters = array( + '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~' + ); + + protected $StrongRegex = array( + '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s', + '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us', + ); + + protected $EmRegex = array( + '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', + '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', + ); + + protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+'; + + protected $voidElements = array( + 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', + ); + + protected $textLevelElements = array( + 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', + 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', + 'i', 'rp', 'del', 'code', 'strike', 'marquee', + 'q', 'rt', 'ins', 'font', 'strong', + 's', 'tt', 'kbd', 'mark', + 'u', 'xm', 'sub', 'nobr', + 'sup', 'ruby', + 'var', 'span', + 'wbr', 'time', + ); +} diff --git a/kirby/i18n/rules/LICENSE b/kirby/i18n/rules/LICENSE new file mode 100755 index 0000000..36c3036 --- /dev/null +++ b/kirby/i18n/rules/LICENSE @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2012-217 Florian Eckerstorfer + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/kirby/i18n/rules/ar.json b/kirby/i18n/rules/ar.json new file mode 100755 index 0000000..e46915f --- /dev/null +++ b/kirby/i18n/rules/ar.json @@ -0,0 +1,30 @@ +{ + "أ" : "a", + "ب" : "b", + "ت" : "t", + "ث" : "th", + "ج" : "g", + "ح" : "h", + "خ" : "kh", + "د" : "d", + "ذ" : "th", + "ر" : "r", + "ز" : "z", + "س" : "s", + "ش" : "sh", + "ص" : "s", + "ض" : "d", + "ط" : "t", + "ظ" : "th", + "ع" : "aa", + "غ" : "gh", + "ف" : "f", + "ق" : "k", + "ك" : "k", + "ل" : "l", + "م" : "m", + "ن" : "n", + "ه" : "h", + "و" : "o", + "ي" : "y" +} diff --git a/kirby/i18n/rules/az.json b/kirby/i18n/rules/az.json new file mode 100755 index 0000000..ad6e2a9 --- /dev/null +++ b/kirby/i18n/rules/az.json @@ -0,0 +1,16 @@ +{ + "Ə": "E", + "Ç": "C", + "Ğ": "G", + "İ": "I", + "Ş": "S", + "Ö": "O", + "Ü": "U", + "ə": "e", + "ç": "c", + "ğ": "g", + "ı": "i", + "ş": "s", + "ö": "o", + "ü": "u" +} diff --git a/kirby/i18n/rules/bg.json b/kirby/i18n/rules/bg.json new file mode 100755 index 0000000..4c45ca1 --- /dev/null +++ b/kirby/i18n/rules/bg.json @@ -0,0 +1,65 @@ +{ + "А": "A", + "Б": "B", + "В": "V", + "Г": "G", + "Д": "D", + "Е": "E", + "Ж": "J", + "З": "Z", + "И": "I", + "Й": "Y", + "К": "K", + "Л": "L", + "М": "M", + "Н": "N", + "О": "O", + "П": "P", + "Р": "R", + "С": "S", + "Т": "T", + "У": "U", + "Ф": "F", + "Х": "H", + "Ц": "Ts", + "Ч": "Ch", + "Ш": "Sh", + "Щ": "Sht", + "Ъ": "A", + "Ь": "I", + "Ю": "Iu", + "Я": "Ia", + "а": "a", + "б": "b", + "в": "v", + "г": "g", + "д": "d", + "е": "e", + "ж": "j", + "з": "z", + "и": "i", + "й": "y", + "к": "k", + "л": "l", + "м": "m", + "н": "n", + "о": "o", + "п": "p", + "р": "r", + "с": "s", + "т": "t", + "у": "u", + "ф": "f", + "х": "h", + "ц": "ts", + "ч": "ch", + "ш": "sh", + "щ": "sht", + "ъ": "a", + "ь": "i", + "ю": "iu", + "я": "ia", + "ия": "ia", + "йо": "iо", + "ьо": "io" +} diff --git a/kirby/i18n/rules/cs.json b/kirby/i18n/rules/cs.json new file mode 100755 index 0000000..549f805 --- /dev/null +++ b/kirby/i18n/rules/cs.json @@ -0,0 +1,20 @@ +{ + "Č": "C", + "Ď": "D", + "Ě": "E", + "Ň": "N", + "Ř": "R", + "Š": "S", + "Ť": "T", + "Ů": "U", + "Ž": "Z", + "č": "c", + "ď": "d", + "ě": "e", + "ň": "n", + "ř": "r", + "š": "s", + "ť": "t", + "ů": "u", + "ž": "z" +} diff --git a/kirby/i18n/rules/da.json b/kirby/i18n/rules/da.json new file mode 100755 index 0000000..b88c17c --- /dev/null +++ b/kirby/i18n/rules/da.json @@ -0,0 +1,10 @@ +{ + "Æ": "Ae", + "æ": "ae", + "Ø": "Oe", + "ø": "oe", + "Å": "Aa", + "å": "aa", + "É": "E", + "é": "e" +} diff --git a/kirby/i18n/rules/de.json b/kirby/i18n/rules/de.json new file mode 100755 index 0000000..881b68c --- /dev/null +++ b/kirby/i18n/rules/de.json @@ -0,0 +1,9 @@ +{ + "Ä": "AE", + "Ö": "OE", + "Ü": "UE", + "ß": "ss", + "ä": "ae", + "ö": "oe", + "ü": "ue" +} diff --git a/kirby/i18n/rules/el.json b/kirby/i18n/rules/el.json new file mode 100755 index 0000000..767a223 --- /dev/null +++ b/kirby/i18n/rules/el.json @@ -0,0 +1,111 @@ +{ + "ΑΥ": "AU", + "Αυ": "Au", + "ΟΥ": "OU", + "Ου": "Ou", + "ΕΥ": "EU", + "Ευ": "Eu", + "ΕΙ": "I", + "Ει": "I", + "ΟΙ": "I", + "Οι": "I", + "ΥΙ": "I", + "Υι": "I", + "ΑΎ": "AU", + "Αύ": "Au", + "ΟΎ": "OU", + "Ού": "Ou", + "ΕΎ": "EU", + "Εύ": "Eu", + "ΕΊ": "I", + "Εί": "I", + "ΟΊ": "I", + "Οί": "I", + "ΎΙ": "I", + "Ύι": "I", + "ΥΊ": "I", + "Υί": "I", + "αυ": "au", + "ου": "ou", + "ευ": "eu", + "ει": "i", + "οι": "i", + "υι": "i", + "αύ": "au", + "ού": "ou", + "εύ": "eu", + "εί": "i", + "οί": "i", + "ύι": "i", + "υί": "i", + "Α": "A", + "Β": "V", + "Γ": "G", + "Δ": "D", + "Ε": "E", + "Ζ": "Z", + "Η": "I", + "Θ": "Th", + "Ι": "I", + "Κ": "K", + "Λ": "L", + "Μ": "M", + "Ν": "N", + "Ξ": "X", + "Ο": "O", + "Π": "P", + "Ρ": "R", + "Σ": "S", + "Τ": "T", + "Υ": "I", + "Φ": "F", + "Χ": "Ch", + "Ψ": "Ps", + "Ω": "O", + "Ά": "A", + "Έ": "E", + "Ή": "I", + "Ί": "I", + "Ό": "O", + "Ύ": "I", + "Ϊ": "I", + "Ϋ": "I", + "ϒ": "I", + "α": "a", + "β": "v", + "γ": "g", + "δ": "d", + "ε": "e", + "ζ": "z", + "η": "i", + "θ": "th", + "ι": "i", + "κ": "k", + "λ": "l", + "μ": "m", + "ν": "n", + "ξ": "x", + "ο": "o", + "π": "p", + "ρ": "r", + "ς": "s", + "σ": "s", + "τ": "t", + "υ": "i", + "φ": "f", + "χ": "ch", + "ψ": "ps", + "ω": "o", + "ά": "a", + "έ": "e", + "ή": "i", + "ί": "i", + "ό": "o", + "ύ": "i", + "ϊ": "i", + "ϋ": "i", + "ΰ": "i", + "ώ": "o", + "ϐ": "v", + "ϑ": "th" +} diff --git a/kirby/i18n/rules/eo.json b/kirby/i18n/rules/eo.json new file mode 100755 index 0000000..9a4e658 --- /dev/null +++ b/kirby/i18n/rules/eo.json @@ -0,0 +1,14 @@ +{ + "ĉ": "cx", + "ĝ": "gx", + "ĥ": "hx", + "ĵ": "jx", + "ŝ": "sx", + "ŭ": "ux", + "Ĉ": "CX", + "Ĝ": "GX", + "Ĥ": "HX", + "Ĵ": "JX", + "Ŝ": "SX", + "Ŭ": "UX" +} diff --git a/kirby/i18n/rules/et.json b/kirby/i18n/rules/et.json new file mode 100755 index 0000000..fcea469 --- /dev/null +++ b/kirby/i18n/rules/et.json @@ -0,0 +1,14 @@ +{ + "Š": "S", + "Ž": "Z", + "Õ": "O", + "Ä": "A", + "Ö": "O", + "Ü": "U", + "š": "s", + "ž": "z", + "õ": "o", + "ä": "a", + "ö": "o", + "ü": "u" +} \ No newline at end of file diff --git a/kirby/i18n/rules/fa.json b/kirby/i18n/rules/fa.json new file mode 100755 index 0000000..0448016 --- /dev/null +++ b/kirby/i18n/rules/fa.json @@ -0,0 +1,36 @@ +{ + "آ" : "A", + "ا" : "a", + "ب" : "b", + "پ" : "p", + "ت" : "t", + "ث" : "th", + "ج" : "j", + "چ" : "ch", + "ح" : "h", + "خ" : "kh", + "د" : "d", + "ذ" : "th", + "ر" : "r", + "ز" : "z", + "ژ" : "zh", + "س" : "s", + "ش" : "sh", + "ص" : "s", + "ض" : "z", + "ط" : "t", + "ظ" : "z", + "ع" : "a", + "غ" : "gh", + "ف" : "f", + "ق" : "g", + "ك" : "k", + "ک" : "k", + "گ" : "g", + "ل" : "l", + "م" : "m", + "ن" : "n", + "و" : "o", + "ه" : "h", + "ی" : "y" +} diff --git a/kirby/i18n/rules/fi.json b/kirby/i18n/rules/fi.json new file mode 100755 index 0000000..fd35423 --- /dev/null +++ b/kirby/i18n/rules/fi.json @@ -0,0 +1,6 @@ +{ + "Ä": "A", + "Ö": "O", + "ä": "a", + "ö": "o" +} diff --git a/kirby/i18n/rules/fr.json b/kirby/i18n/rules/fr.json new file mode 100755 index 0000000..29c94b9 --- /dev/null +++ b/kirby/i18n/rules/fr.json @@ -0,0 +1,34 @@ +{ + "À": "A", + "Â": "A", + "Æ": "AE", + "Ç": "C", + "É": "E", + "È": "E", + "Ê": "E", + "Ë": "E", + "Ï": "I", + "Î": "I", + "Ô": "O", + "Œ": "OE", + "Ù": "U", + "Û": "U", + "Ü": "U", + "à": "a", + "â": "a", + "æ": "ae", + "ç": "c", + "é": "e", + "è": "e", + "ê": "e", + "ë": "e", + "ï": "i", + "î": "i", + "ô": "o", + "œ": "oe", + "ù": "u", + "û": "u", + "ü": "u", + "ÿ": "y", + "Ÿ": "Y" +} diff --git a/kirby/i18n/rules/hi.json b/kirby/i18n/rules/hi.json new file mode 100755 index 0000000..f653f15 --- /dev/null +++ b/kirby/i18n/rules/hi.json @@ -0,0 +1,66 @@ +{ + "अ": "a", + "आ": "aa", + "ए": "e", + "ई": "ii", + "ऍ": "ei", + "ऎ": "ae", + "ऐ": "ai", + "इ": "i", + "ओ": "o", + "ऑ": "oi", + "ऒ": "oii", + "ऊ": "uu", + "औ": "ou", + "उ": "u", + "ब": "B", + "भ": "Bha", + "च": "Ca", + "छ": "Chha", + "ड": "Da", + "ढ": "Dha", + "फ": "Fa", + "फ़": "Fi", + "ग": "Ga", + "घ": "Gha", + "ग़": "Ghi", + "ह": "Ha", + "ज": "Ja", + "झ": "Jha", + "क": "Ka", + "ख": "Kha", + "ख़": "Khi", + "ल": "L", + "ळ": "Li", + "ऌ": "Li", + "ऴ": "Lii", + "ॡ": "Lii", + "म": "Ma", + "न": "Na", + "ङ": "Na", + "ञ": "Nia", + "ण": "Nae", + "ऩ": "Ni", + "ॐ": "oms", + "प": "Pa", + "क़": "Qi", + "र": "Ra", + "ऋ": "Ri", + "ॠ": "Ri", + "ऱ": "Ri", + "स": "Sa", + "श": "Sha", + "ष": "Shha", + "ट": "Ta", + "त": "Ta", + "ठ": "Tha", + "द": "Tha", + "थ": "Tha", + "ध": "Thha", + "ड़": "ugDha", + "ढ़": "ugDhha", + "व": "Va", + "य": "Ya", + "य़": "Yi", + "ज़": "Za" +} diff --git a/kirby/i18n/rules/hr.json b/kirby/i18n/rules/hr.json new file mode 100755 index 0000000..bf2b10d --- /dev/null +++ b/kirby/i18n/rules/hr.json @@ -0,0 +1,12 @@ +{ + "Č": "C", + "Ć": "C", + "Ž": "Z", + "Š": "S", + "Đ": "Dj", + "č": "c", + "ć": "c", + "ž": "z", + "š": "s", + "đ": "dj" +} \ No newline at end of file diff --git a/kirby/i18n/rules/hu.json b/kirby/i18n/rules/hu.json new file mode 100755 index 0000000..2bb2f3a --- /dev/null +++ b/kirby/i18n/rules/hu.json @@ -0,0 +1,20 @@ +{ + "Á": "a", + "É": "e", + "Í": "i", + "Ó": "o", + "Ö": "o", + "Ő": "o", + "Ú": "u", + "Ü": "u", + "Ű": "u", + "á": "a", + "é": "e", + "í": "i", + "ó": "o", + "ö": "o", + "ő": "o", + "ú": "u", + "ü": "u", + "ű": "u" +} diff --git a/kirby/i18n/rules/hy.json b/kirby/i18n/rules/hy.json new file mode 100755 index 0000000..08188e6 --- /dev/null +++ b/kirby/i18n/rules/hy.json @@ -0,0 +1,79 @@ +{ + "Ա": "A", + "Բ": "B", + "Գ": "G", + "Դ": "D", + "Ե": "E", + "Զ": "Z", + "Է": "E", + "Ը": "Y", + "Թ": "Th", + "Ժ": "Zh", + "Ի": "I", + "Լ": "L", + "Խ": "Kh", + "Ծ": "Ts", + "Կ": "K", + "Հ": "H", + "Ձ": "Dz", + "Ղ": "Gh", + "Ճ": "Tch", + "Մ": "M", + "Յ": "Y", + "Ն": "N", + "Շ": "Sh", + "Ո": "Vo", + "Չ": "Ch", + "Պ": "P", + "Ջ": "J", + "Ռ": "R", + "Ս": "S", + "Վ": "V", + "Տ": "T", + "Ր": "R", + "Ց": "C", + "Ւ": "u", + "Փ": "Ph", + "Ք": "Q", + "և": "ev", + "Օ": "O", + "Ֆ": "F", + "ա": "a", + "բ": "b", + "գ": "g", + "դ": "d", + "ե": "e", + "զ": "z", + "է": "e", + "ը": "y", + "թ": "th", + "ժ": "zh", + "ի": "i", + "լ": "l", + "խ": "kh", + "ծ": "ts", + "կ": "k", + "հ": "h", + "ձ": "dz", + "ղ": "gh", + "ճ": "tch", + "մ": "m", + "յ": "y", + "ն": "n", + "շ": "sh", + "ո": "vo", + "չ": "ch", + "պ": "p", + "ջ": "j", + "ռ": "r", + "ս": "s", + "վ": "v", + "տ": "t", + "ր": "r", + "ց": "c", + "ւ": "u", + "փ": "ph", + "ք": "q", + "օ": "o", + "ֆ": "f" +} diff --git a/kirby/i18n/rules/it.json b/kirby/i18n/rules/it.json new file mode 100755 index 0000000..647c2cf --- /dev/null +++ b/kirby/i18n/rules/it.json @@ -0,0 +1,13 @@ +{ + "À": "a", + "È": "e", + "Ì": "i", + "Ò": "o", + "Ù": "u", + "à": "a", + "é": "e", + "è": "e", + "ì": "i", + "ò": "o", + "ù": "u" +} diff --git a/kirby/i18n/rules/ja.json b/kirby/i18n/rules/ja.json new file mode 100755 index 0000000..dd0c615 --- /dev/null +++ b/kirby/i18n/rules/ja.json @@ -0,0 +1,166 @@ +{ + "きゃ": "kya", + "しゃ": "sha", + "ちゃ": "cha", + "にゃ": "nya", + "ひゃ": "hya", + "みゃ": "mya", + "りゃ": "rya", + "ぎゃ": "gya", + "じゃ": "ja", + "ぢゃ": "ja", + "びゃ": "bya", + "ぴゃ": "pya", + + "きゅ": "kyu", + "しゅ": "shu", + "ちゅ": "chu", + "にゅ": "nyu", + "ひゅ": "hyu", + "みゅ": "myu", + "りゅ": "ryu", + "ぎゅ": "gyu", + "じゅ": "ju", + "ぢゅ": "ju", + "びゅ": "byu", + "ぴゅ": "pyu", + + "きょ": "kyo", + "しょ": "sho", + "ちょ": "cho", + "にょ": "nyo", + "ひょ": "hyo", + "みょ": "myo", + "りょ": "ryo", + "ぎょ": "gyo", + "じょ": "jo", + "ぢょ": "jo", + "びょ": "byo", + "ぴょ": "pyo", + + "あ": "a", + "ア": "a", + "か": "ka", + "カ": "ka", + "さ": "sa", + "サ": "sa", + "た": "ta", + "タ": "ta", + "な": "na", + "ナ": "na", + "は": "ha", + "ハ": "ha", + "ま": "ma", + "マ": "ma", + "や": "ya", + "ヤ": "ya", + "ら": "ra", + "ラ": "ra", + "わ": "wa", + "ワ": "wa", + "が": "ga", + "ざ": "za", + "だ": "da", + "ば": "ba", + "ぱ": "pa", + + "い": "i", + "イ": "i", + "き": "ki", + "キ": "ki", + "し": "shi", + "シ": "shi", + "ち": "chi", + "チ": "chi", + "に": "ni", + "ニ": "ni", + "ひ": "hi", + "ヒ": "hi", + "み": "mi", + "ミ": "mi", + "り": "ri", + "リ": "ri", + "ゐ": "wi", + "ヰ": "wi", + "ぎ": "gi", + "じ": "dji", + "ぢ": "ji", + "び": "bi", + "ぴ": "pi", + + "う": "u", + "ウ": "u", + "く": "ku", + "ク": "ku", + "す": "su", + "ス": "su", + "つ": "tsu", + "ツ": "tsu", + "ぬ": "nu", + "ヌ": "nu", + "ふ": "fu", + "フ": "fu", + "む": "mu", + "ム": "mu", + "ゆ": "yu", + "ユ": "yu", + "る": "ru", + "ル": "ru", + "ぐ": "gu", + "ず": "zu", + "づ": "dzu", + "ぶ": "bu", + "ぷ": "pu", + + "え": "e", + "エ": "e", + "け": "ke", + "ケ": "ke", + "せ": "se", + "セ": "se", + "て": "te", + "テ": "te", + "ね": "ne", + "ネ": "ne", + "へ": "he", + "ヘ": "he", + "め": "me", + "メ": "me", + "れ": "re", + "レ": "re", + "ゑ": "we", + "ヱ": "we", + "げ": "ge", + "ぜ": "ze", + "で": "de", + "べ": "be", + "ぺ": "pe", + + "お": "o", + "オ": "o", + "こ": "ko", + "コ": "ko", + "そ": "so", + "ソ": "so", + "と": "to", + "ト": "to", + "の": "no", + "ノ": "no", + "ほ": "ho", + "ホ": "ho", + "も": "mo", + "モ": "mo", + "よ": "yo", + "ヨ": "yo", + "ろ": "ro", + "ロ": "ro", + "を": "wo", + "ヲ": "wo", + "ん": "n", + "ン": "n", + "ご": "go", + "ぞ": "zo", + "ど": "do", + "ぼ": "bo", + "ぽ": "po" +} diff --git a/kirby/i18n/rules/ka.json b/kirby/i18n/rules/ka.json new file mode 100755 index 0000000..2c63573 --- /dev/null +++ b/kirby/i18n/rules/ka.json @@ -0,0 +1,35 @@ +{ + "ა": "a", + "ბ": "b", + "გ": "g", + "დ": "d", + "ე": "e", + "ვ": "v", + "ზ": "z", + "თ": "t", + "ი": "i", + "კ": "k", + "ლ": "l", + "მ": "m", + "ნ": "n", + "ო": "o", + "პ": "p", + "ჟ": "zh", + "რ": "r", + "ს": "s", + "ტ": "t", + "უ": "u", + "ფ": "f", + "ქ": "k", + "ღ": "gh", + "ყ": "q", + "შ": "sh", + "ჩ": "ch", + "ც": "ts", + "ძ": "dz", + "წ": "ts", + "ჭ": "ch", + "ხ": "kh", + "ჯ": "j", + "ჰ": "h" +} diff --git a/kirby/i18n/rules/lt.json b/kirby/i18n/rules/lt.json new file mode 100755 index 0000000..23e0d70 --- /dev/null +++ b/kirby/i18n/rules/lt.json @@ -0,0 +1,20 @@ +{ + "Ą": "A", + "Č": "C", + "Ę": "E", + "Ė": "E", + "Į": "I", + "Š": "S", + "Ų": "U", + "Ū": "U", + "Ž": "Z", + "ą": "a", + "č": "c", + "ę": "e", + "ė": "e", + "į": "i", + "š": "s", + "ų": "u", + "ū": "u", + "ž": "z" +} diff --git a/kirby/i18n/rules/lv.json b/kirby/i18n/rules/lv.json new file mode 100755 index 0000000..d5b0010 --- /dev/null +++ b/kirby/i18n/rules/lv.json @@ -0,0 +1,18 @@ +{ + "Ā": "A", + "Ē": "E", + "Ģ": "G", + "Ī": "I", + "Ķ": "K", + "Ļ": "L", + "Ņ": "N", + "Ū": "U", + "ā": "a", + "ē": "e", + "ģ": "g", + "ī": "i", + "ķ": "k", + "ļ": "l", + "ņ": "n", + "ū": "u" +} diff --git a/kirby/i18n/rules/mk.json b/kirby/i18n/rules/mk.json new file mode 100755 index 0000000..7a87f46 --- /dev/null +++ b/kirby/i18n/rules/mk.json @@ -0,0 +1,64 @@ +{ + "А": "A", + "Б": "B", + "В": "V", + "Г": "G", + "Д": "D", + "Ѓ": "Gj", + "Е": "E", + "Ж": "Zh", + "З": "Z", + "Ѕ": "Dz", + "И": "I", + "Ј": "J", + "К": "K", + "Л": "L", + "Љ": "Lj", + "М": "M", + "Н": "N", + "Њ": "Nj", + "О": "O", + "П": "P", + "Р": "R", + "С": "S", + "Т": "T", + "Ќ": "Kj", + "У": "U", + "Ф": "F", + "Х": "H", + "Ц": "C", + "Ч": "Ch", + "Џ": "Dj", + "Ш": "Sh", + "а": "a", + "б": "b", + "в": "v", + "г": "g", + "д": "d", + "ѓ": "gj", + "е": "e", + "ж": "zh", + "з": "z", + "ѕ": "dz", + "и": "i", + "ј": "j", + "к": "k", + "л": "l", + "љ": "lj", + "м": "m", + "н": "n", + "њ": "nj", + "о": "o", + "п": "p", + "р": "r", + "с": "s", + "т": "t", + "ќ": "kj", + "у": "u", + "ф": "f", + "х": "h", + "ц": "c", + "ч": "ch", + "џ": "dj", + "ш": "sh" +} diff --git a/kirby/i18n/rules/my.json b/kirby/i18n/rules/my.json new file mode 100755 index 0000000..08f5a0a --- /dev/null +++ b/kirby/i18n/rules/my.json @@ -0,0 +1,121 @@ +{ + "က": "k", + "ခ": "kh", + "ဂ": "g", + "ဃ": "ga", + "င": "ng", + "စ": "s", + "ဆ": "sa", + "ဇ": "z", + "စျ" : "za", + "ည": "ny", + "ဋ": "t", + "ဌ": "ta", + "ဍ": "d", + "ဎ": "da", + "ဏ": "na", + "တ": "t", + "ထ": "ta", + "ဒ": "d", + "ဓ": "da", + "န": "n", + "ပ": "p", + "ဖ": "pa", + "ဗ": "b", + "ဘ": "ba", + "မ": "m", + "ယ": "y", + "ရ": "ya", + "လ": "l", + "ဝ": "w", + "သ": "th", + "ဟ": "h", + "ဠ": "la", + "အ": "a", + + "ြ": "y", + "ျ": "ya", + "ွ": "w", + "ြွ": "yw", + "ျွ": "ywa", + "ှ": "h", + + "ဧ": "e", + "၏": "-e", + "ဣ": "i", + "ဤ": "-i", + "ဉ": "u", + "ဦ": "-u", + "ဩ": "aw", + "သြော" : "aw", + "ဪ": "aw", + "၍": "ywae", + "၌": "hnaik", + + "၀": "0", + "၁": "1", + "၂": "2", + "၃": "3", + "၄": "4", + "၅": "5", + "၆": "6", + "၇": "7", + "၈": "8", + "၉": "9", + + "္": "", + "့": "", + "း": "", + + "ာ": "a", + "ါ": "a", + "ေ": "e", + "ဲ": "e", + "ိ": "i", + "ီ": "i", + "ို": "o", + "ု": "u", + "ူ": "u", + "ေါင်": "aung", + "ော": "aw", + "ော်": "aw", + "ေါ": "aw", + "ေါ်": "aw", + "်": "at", + "က်": "et", + "ိုက်" : "aik", + "ောက်" : "auk", + "င်" : "in", + "ိုင်" : "aing", + "ောင်" : "aung", + "စ်" : "it", + "ည်" : "i", + "တ်" : "at", + "ိတ်" : "eik", + "ုတ်" : "ok", + "ွတ်" : "ut", + "ေတ်" : "it", + "ဒ်" : "d", + "ိုဒ်" : "ok", + "ုဒ်" : "ait", + "န်" : "an", + "ာန်" : "an", + "ိန်" : "ein", + "ုန်" : "on", + "ွန်" : "un", + "ပ်" : "at", + "ိပ်" : "eik", + "ုပ်" : "ok", + "ွပ်" : "ut", + "န်ုပ်" : "nub", + "မ်" : "an", + "ိမ်" : "ein", + "ုမ်" : "on", + "ွမ်" : "un", + "ယ်" : "e", + "ိုလ်" : "ol", + "ဉ်" : "in", + "ံ": "an", + "ိံ" : "ein", + "ုံ" : "on" +} diff --git a/kirby/i18n/rules/nb.json b/kirby/i18n/rules/nb.json new file mode 100755 index 0000000..66000ba --- /dev/null +++ b/kirby/i18n/rules/nb.json @@ -0,0 +1,8 @@ +{ + "Æ": "AE", + "Ø": "OE", + "Å": "AA", + "æ": "ae", + "ø": "oe", + "å": "aa" +} diff --git a/kirby/i18n/rules/pl.json b/kirby/i18n/rules/pl.json new file mode 100755 index 0000000..5d0c123 --- /dev/null +++ b/kirby/i18n/rules/pl.json @@ -0,0 +1,20 @@ +{ + "Ą": "A", + "Ć": "C", + "Ę": "E", + "Ł": "L", + "Ń": "N", + "Ó": "O", + "Ś": "S", + "Ź": "Z", + "Ż": "Z", + "ą": "a", + "ć": "c", + "ę": "e", + "ł": "l", + "ń": "n", + "ó": "o", + "ś": "s", + "ź": "z", + "ż": "z" +} diff --git a/kirby/i18n/rules/pt_BR.json b/kirby/i18n/rules/pt_BR.json new file mode 100755 index 0000000..39bca6c --- /dev/null +++ b/kirby/i18n/rules/pt_BR.json @@ -0,0 +1,187 @@ + +{ + "°": "0", + "¹": "1", + "²": "2", + "³": "3", + "⁴": "4", + "⁵": "5", + "⁶": "6", + "⁷": "7", + "⁸": "8", + "⁹": "9", + + "₀": "0", + "₁": "1", + "₂": "2", + "₃": "3", + "₄": "4", + "₅": "5", + "₆": "6", + "₇": "7", + "₈": "8", + "₉": "9", + + + "æ": "ae", + "ǽ": "ae", + "À": "A", + "Á": "A", + "Â": "A", + "Ã": "A", + "Å": "AA", + "Ǻ": "A", + "Ă": "A", + "Ǎ": "A", + "Æ": "AE", + "Ǽ": "AE", + "à": "a", + "á": "a", + "â": "a", + "ã": "a", + "å": "aa", + "ǻ": "a", + "ă": "a", + "ǎ": "a", + "ª": "a", + "@": "at", + "Ĉ": "C", + "Ċ": "C", + "Ç": "Ç", + "ç": "ç", + "ĉ": "c", + "ċ": "c", + "©": "c", + "Ð": "Dj", + "Đ": "D", + "ð": "dj", + "đ": "d", + "È": "E", + "É": "E", + "Ê": "E", + "Ë": "E", + "Ĕ": "E", + "Ė": "E", + "è": "e", + "é": "é", + "ê": "e", + "ë": "e", + "ĕ": "e", + "ė": "e", + "ƒ": "f", + "Ĝ": "G", + "Ġ": "G", + "ĝ": "g", + "ġ": "g", + "Ĥ": "H", + "Ħ": "H", + "ĥ": "h", + "ħ": "h", + "Ì": "I", + "Í": "I", + "Î": "I", + "Ï": "I", + "Ĩ": "I", + "Ĭ": "I", + "Ǐ": "I", + "Į": "I", + "IJ": "IJ", + "ì": "i", + "í": "i", + "î": "i", + "ï": "i", + "ĩ": "i", + "ĭ": "i", + "ǐ": "i", + "į": "i", + "ij": "ij", + "Ĵ": "J", + "ĵ": "j", + "Ĺ": "L", + "Ľ": "L", + "Ŀ": "L", + "ĺ": "l", + "ľ": "l", + "ŀ": "l", + "Ñ": "N", + "ñ": "n", + "ʼn": "n", + "Ò": "O", + "Ó": "O", + "Ô": "O", + "Õ": "O", + "Ō": "O", + "Ŏ": "O", + "Ǒ": "O", + "Ő": "O", + "Ơ": "O", + "Ø": "OE", + "Ǿ": "O", + "Œ": "OE", + "ò": "o", + "ó": "o", + "ô": "o", + "õ": "o", + "ō": "o", + "ŏ": "o", + "ǒ": "o", + "ő": "o", + "ơ": "o", + "ø": "oe", + "ǿ": "o", + "º": "o", + "œ": "oe", + "Ŕ": "R", + "Ŗ": "R", + "ŕ": "r", + "ŗ": "r", + "Ŝ": "S", + "Ș": "S", + "ŝ": "s", + "ș": "s", + "ſ": "s", + "Ţ": "T", + "Ț": "T", + "Ŧ": "T", + "Þ": "TH", + "ţ": "t", + "ț": "t", + "ŧ": "t", + "þ": "th", + "Ù": "U", + "Ú": "U", + "Û": "U", + "Ü": "U", + "Ũ": "U", + "Ŭ": "U", + "Ű": "U", + "Ų": "U", + "Ư": "U", + "Ǔ": "U", + "Ǖ": "U", + "Ǘ": "U", + "Ǚ": "U", + "Ǜ": "U", + "ù": "u", + "ú": "u", + "û": "u", + "ü": "u", + "ũ": "u", + "ŭ": "u", + "ű": "u", + "ų": "u", + "ư": "u", + "ǔ": "u", + "ǖ": "u", + "ǘ": "u", + "ǚ": "u", + "ǜ": "u", + "Ŵ": "W", + "ŵ": "w", + "Ý": "Y", + "Ÿ": "Y", + "Ŷ": "Y", + "ý": "y", + "ÿ": "y", + "ŷ": "y" +} diff --git a/kirby/i18n/rules/rm.json b/kirby/i18n/rules/rm.json new file mode 100755 index 0000000..47b9d9b --- /dev/null +++ b/kirby/i18n/rules/rm.json @@ -0,0 +1,16 @@ +{ + "ă": "a", + "î": "i", + "â": "a", + "ş": "s", + "ș": "s", + "ţ": "t", + "ț": "t", + "Ă": "A", + "Î": "I", + "Â": "A", + "Ş": "S", + "Ș": "S", + "Ţ": "T", + "Ț": "T" +} diff --git a/kirby/i18n/rules/ru.json b/kirby/i18n/rules/ru.json new file mode 100755 index 0000000..b8b354c --- /dev/null +++ b/kirby/i18n/rules/ru.json @@ -0,0 +1,68 @@ +{ + "Ъ": "", + "Ь": "", + "А": "A", + "Б": "B", + "Ц": "C", + "Ч": "Ch", + "Д": "D", + "Е": "E", + "Ё": "E", + "Э": "E", + "Ф": "F", + "Г": "G", + "Х": "H", + "И": "I", + "Й": "Y", + "Я": "Ya", + "Ю": "Yu", + "К": "K", + "Л": "L", + "М": "M", + "Н": "N", + "О": "O", + "П": "P", + "Р": "R", + "С": "S", + "Ш": "Sh", + "Щ": "Shch", + "Т": "T", + "У": "U", + "В": "V", + "Ы": "Y", + "З": "Z", + "Ж": "Zh", + "ъ": "", + "ь": "", + "а": "a", + "б": "b", + "ц": "c", + "ч": "ch", + "д": "d", + "е": "e", + "ё": "e", + "э": "e", + "ф": "f", + "г": "g", + "х": "h", + "и": "i", + "й": "y", + "я": "ya", + "ю": "yu", + "к": "k", + "л": "l", + "м": "m", + "н": "n", + "о": "o", + "п": "p", + "р": "r", + "с": "s", + "ш": "sh", + "щ": "shch", + "т": "t", + "у": "u", + "в": "v", + "ы": "y", + "з": "z", + "ж": "zh" +} diff --git a/kirby/i18n/rules/sr.json b/kirby/i18n/rules/sr.json new file mode 100755 index 0000000..f4c11db --- /dev/null +++ b/kirby/i18n/rules/sr.json @@ -0,0 +1,72 @@ +{ + "а": "a", + "б": "b", + "в": "v", + "г": "g", + "д": "d", + "ђ": "dj", + "е": "e", + "ж": "z", + "з": "z", + "и": "i", + "ј": "j", + "к": "k", + "л": "l", + "љ": "lj", + "м": "m", + "н": "n", + "њ": "nj", + "о": "o", + "п": "p", + "р": "r", + "с": "s", + "т": "t", + "ћ": "c", + "у": "u", + "ф": "f", + "х": "h", + "ц": "c", + "ч": "c", + "џ": "dz", + "ш": "s", + "А": "A", + "Б": "B", + "В": "V", + "Г": "G", + "Д": "D", + "Ђ": "Dj", + "Е": "E", + "Ж": "Z", + "З": "Z", + "И": "I", + "Ј": "J", + "К": "K", + "Л": "L", + "Љ": "Lj", + "М": "M", + "Н": "N", + "Њ": "Nj", + "О": "O", + "П": "P", + "Р": "R", + "С": "S", + "Т": "T", + "Ћ": "C", + "У": "U", + "Ф": "F", + "Х": "H", + "Ц": "C", + "Ч": "C", + "Џ": "Dz", + "Ш": "S", + "š": "s", + "đ": "dj", + "ž": "z", + "ć": "c", + "č": "c", + "Š": "S", + "Đ": "DJ", + "Ž": "Z", + "Ć": "C", + "Č": "C" +} \ No newline at end of file diff --git a/kirby/i18n/rules/sv_SE.json b/kirby/i18n/rules/sv_SE.json new file mode 100755 index 0000000..a22f3eb --- /dev/null +++ b/kirby/i18n/rules/sv_SE.json @@ -0,0 +1,8 @@ +{ + "Ä": "A", + "Å": "a", + "Ö": "O", + "ä": "a", + "å": "a", + "ö": "o" +} diff --git a/kirby/i18n/rules/tr.json b/kirby/i18n/rules/tr.json new file mode 100755 index 0000000..07fbae5 --- /dev/null +++ b/kirby/i18n/rules/tr.json @@ -0,0 +1,14 @@ +{ + "Ç": "C", + "Ğ": "G", + "İ": "I", + "Ş": "S", + "Ö": "O", + "Ü": "U", + "ç": "c", + "ğ": "g", + "ı": "i", + "ş": "s", + "ö": "o", + "ü": "u" +} diff --git a/kirby/i18n/rules/uk.json b/kirby/i18n/rules/uk.json new file mode 100755 index 0000000..673b7ed --- /dev/null +++ b/kirby/i18n/rules/uk.json @@ -0,0 +1,10 @@ +{ + "Ґ": "G", + "І": "I", + "Ї": "Ji", + "Є": "Ye", + "ґ": "g", + "і": "i", + "ї": "ji", + "є": "ye" +} diff --git a/kirby/i18n/rules/vi.json b/kirby/i18n/rules/vi.json new file mode 100755 index 0000000..fdeff69 --- /dev/null +++ b/kirby/i18n/rules/vi.json @@ -0,0 +1,135 @@ +{ + "à": "a", + "ạ": "a", + "á": "a", + "ả": "a", + "ã": "a", + "â": "a", + "ầ": "a", + "ấ": "a", + "ậ": "a", + "ẩ": "a", + "ẫ": "a", + "ă": "a", + "ằ": "a", + "ắ": "a", + "ặ": "a", + "ẳ": "a", + "ẵ": "a", + "è": "e", + "é": "e", + "ẹ": "e", + "ẻ": "e", + "ẽ": "e", + "ê": "e", + "ề": "e", + "ế": "e", + "ệ": "e", + "ể": "e", + "ễ": "e", + "ì": "i", + "í": "i", + "ị": "i", + "ỉ": "i", + "ĩ": "i", + "ò": "o", + "ó": "o", + "ọ": "o", + "ỏ": "o", + "õ": "o", + "ô": "o", + "ồ": "o", + "ố": "o", + "ộ": "o", + "ổ": "o", + "ỗ": "o", + "ơ": "o", + "ờ": "o", + "ớ": "o", + "ợ": "o", + "ở": "o", + "ỡ": "o", + "ù": "u", + "ú": "u", + "ụ": "u", + "ủ": "u", + "ũ": "u", + "ư": "u", + "ừ": "u", + "ứ": "u", + "ự": "u", + "ử": "u", + "ữ": "u", + "y": "y", + "ỳ": "y", + "ý": "y", + "ỵ": "y", + "ỷ": "y", + "ỹ": "y", + "À": "A", + "Á": "A", + "Ạ": "A", + "Ả": "A", + "Ã": "A", + "Â": "A", + "Ầ": "A", + "Ấ": "A", + "Ậ": "A", + "Ẩ": "A", + "Ẫ": "A", + "Ă": "A", + "Ằ": "A", + "Ắ": "A", + "Ặ": "A", + "Ẳ": "A", + "Ẵ": "A", + "È": "E", + "É": "E", + "Ẹ": "E", + "Ẻ": "E", + "Ẽ": "E", + "Ê": "E", + "Ề": "E", + "Ế": "E", + "Ệ": "E", + "Ể": "E", + "Ễ": "E", + "Ì": "I", + "Í": "I", + "Ị": "I", + "Ỉ": "I", + "Ĩ": "I", + "Ò": "O", + "Ó": "O", + "Ọ": "O", + "Ỏ": "O", + "Õ": "O", + "Ô": "O", + "Ồ": "O", + "Ố": "O", + "Ộ": "O", + "Ổ": "O", + "Ỗ": "O", + "Ơ": "O", + "Ờ": "O", + "Ớ": "O", + "Ợ": "O", + "Ở": "O", + "Ỡ": "O", + "Ù": "U", + "Ụ": "U", + "Ủ": "U", + "Ũ": "U", + "Ư": "U", + "Ừ": "U", + "Ứ": "U", + "Ự": "U", + "Ử": "U", + "Ữ": "U", + "Y": "Y", + "Ỳ": "Y", + "Ý": "Y", + "Ỵ": "Y", + "Ỷ": "Y", + "Ỹ": "Y" +} diff --git a/kirby/i18n/rules/zh.json b/kirby/i18n/rules/zh.json new file mode 100755 index 0000000..21ec594 --- /dev/null +++ b/kirby/i18n/rules/zh.json @@ -0,0 +1,6937 @@ +{ + "腌" : "yan", + "嗄" : "a", + "迫" : "po", + "捱" : "ai", + "艾" : "ai", + "瑷" : "ai", + "嗌" : "ai", + "犴" : "an", + "鳌" : "ao", + "廒" : "ao", + "拗" : "niu", + "岙" : "ao", + "鏊" : "ao", + "扒" : "ba", + "岜" : "ba", + "耙" : "pa", + "鲅" : "ba", + "癍" : "ban", + "膀" : "pang", + "磅" : "bang", + "炮" : "pao", + "曝" : "pu", + "刨" : "pao", + "瀑" : "pu", + "陂" : "bei", + "埤" : "pi", + "鹎" : "bei", + "邶" : "bei", + "孛" : "bei", + "鐾" : "bei", + "鞴" : "bei", + "畚" : "ben", + "甏" : "beng", + "舭" : "bi", + "秘" : "mi", + "辟" : "pi", + "泌" : "mi", + "裨" : "bi", + "濞" : "bi", + "庳" : "bi", + "嬖" : "bi", + "畀" : "bi", + "筚" : "bi", + "箅" : "bi", + "襞" : "bi", + "跸" : "bi", + "笾" : "bian", + "扁" : "bian", + "碥" : "bian", + "窆" : "bian", + "便" : "bian", + "弁" : "bian", + "缏" : "bian", + "骠" : "biao", + "杓" : "shao", + "飚" : "biao", + "飑" : "biao", + "瘭" : "biao", + "髟" : "biao", + "玢" : "bin", + "豳" : "bin", + "镔" : "bin", + "膑" : "bin", + "屏" : "ping", + "泊" : "bo", + "逋" : "bu", + "晡" : "bu", + "钸" : "bu", + "醭" : "bu", + "埔" : "pu", + "瓿" : "bu", + "礤" : "ca", + "骖" : "can", + "藏" : "cang", + "艚" : "cao", + "侧" : "ce", + "喳" : "zha", + "刹" : "sha", + "瘥" : "chai", + "禅" : "chan", + "廛" : "chan", + "镡" : "tan", + "澶" : "chan", + "躔" : "chan", + "阊" : "chang", + "鲳" : "chang", + "长" : "chang", + "苌" : "chang", + "氅" : "chang", + "鬯" : "chang", + "焯" : "chao", + "朝" : "chao", + "车" : "che", + "琛" : "chen", + "谶" : "chen", + "榇" : "chen", + "蛏" : "cheng", + "埕" : "cheng", + "枨" : "cheng", + "塍" : "cheng", + "裎" : "cheng", + "螭" : "chi", + "眵" : "chi", + "墀" : "chi", + "篪" : "chi", + "坻" : "di", + "瘛" : "chi", + "种" : "zhong", + "重" : "zhong", + "仇" : "chou", + "帱" : "chou", + "俦" : "chou", + "雠" : "chou", + "臭" : "chou", + "楮" : "chu", + "畜" : "chu", + "嘬" : "zuo", + "膪" : "chuai", + "巛" : "chuan", + "椎" : "zhui", + "呲" : "ci", + "兹" : "zi", + "伺" : "si", + "璁" : "cong", + "楱" : "cou", + "攒" : "zan", + "爨" : "cuan", + "隹" : "zhui", + "榱" : "cui", + "撮" : "cuo", + "鹾" : "cuo", + "嗒" : "da", + "哒" : "da", + "沓" : "ta", + "骀" : "tai", + "绐" : "dai", + "埭" : "dai", + "甙" : "dai", + "弹" : "dan", + "澹" : "dan", + "叨" : "dao", + "纛" : "dao", + "簦" : "deng", + "提" : "ti", + "翟" : "zhai", + "绨" : "ti", + "丶" : "dian", + "佃" : "dian", + "簟" : "dian", + "癜" : "dian", + "调" : "tiao", + "铞" : "diao", + "佚" : "yi", + "堞" : "die", + "瓞" : "die", + "揲" : "die", + "垤" : "die", + "疔" : "ding", + "岽" : "dong", + "硐" : "dong", + "恫" : "dong", + "垌" : "dong", + "峒" : "dong", + "芏" : "du", + "煅" : "duan", + "碓" : "dui", + "镦" : "dui", + "囤" : "tun", + "铎" : "duo", + "缍" : "duo", + "驮" : "tuo", + "沲" : "tuo", + "柁" : "tuo", + "哦" : "o", + "恶" : "e", + "轭" : "e", + "锷" : "e", + "鹗" : "e", + "阏" : "e", + "诶" : "ea", + "鲕" : "er", + "珥" : "er", + "佴" : "er", + "番" : "fan", + "彷" : "pang", + "霏" : "fei", + "蜚" : "fei", + "鲱" : "fei", + "芾" : "fei", + "瀵" : "fen", + "鲼" : "fen", + "否" : "fou", + "趺" : "fu", + "桴" : "fu", + "莩" : "fu", + "菔" : "fu", + "幞" : "fu", + "郛" : "fu", + "绂" : "fu", + "绋" : "fu", + "祓" : "fu", + "砩" : "fu", + "黻" : "fu", + "罘" : "fu", + "蚨" : "fu", + "脯" : "pu", + "滏" : "fu", + "黼" : "fu", + "鲋" : "fu", + "鳆" : "fu", + "咖" : "ka", + "噶" : "ga", + "轧" : "zha", + "陔" : "gai", + "戤" : "gai", + "扛" : "kang", + "戆" : "gang", + "筻" : "gang", + "槔" : "gao", + "藁" : "gao", + "缟" : "gao", + "咯" : "ge", + "仡" : "yi", + "搿" : "ge", + "塥" : "ge", + "鬲" : "ge", + "哿" : "ge", + "句" : "ju", + "缑" : "gou", + "鞲" : "gou", + "笱" : "gou", + "遘" : "gou", + "瞽" : "gu", + "罟" : "gu", + "嘏" : "gu", + "牿" : "gu", + "鲴" : "gu", + "栝" : "kuo", + "莞" : "guan", + "纶" : "lun", + "涫" : "guan", + "涡" : "wo", + "呙" : "guo", + "馘" : "guo", + "猓" : "guo", + "咳" : "ke", + "氦" : "hai", + "颔" : "han", + "吭" : "keng", + "颃" : "hang", + "巷" : "xiang", + "蚵" : "ke", + "翮" : "he", + "吓" : "xia", + "桁" : "heng", + "泓" : "hong", + "蕻" : "hong", + "黉" : "hong", + "後" : "hou", + "唿" : "hu", + "煳" : "hu", + "浒" : "hu", + "祜" : "hu", + "岵" : "hu", + "鬟" : "huan", + "圜" : "huan", + "郇" : "xun", + "锾" : "huan", + "逭" : "huan", + "咴" : "hui", + "虺" : "hui", + "会" : "hui", + "溃" : "kui", + "哕" : "hui", + "缋" : "hui", + "锪" : "huo", + "蠖" : "huo", + "缉" : "ji", + "稽" : "ji", + "赍" : "ji", + "丌" : "ji", + "咭" : "ji", + "亟" : "ji", + "殛" : "ji", + "戢" : "ji", + "嵴" : "ji", + "蕺" : "ji", + "系" : "xi", + "蓟" : "ji", + "霁" : "ji", + "荠" : "qi", + "跽" : "ji", + "哜" : "ji", + "鲚" : "ji", + "洎" : "ji", + "芰" : "ji", + "茄" : "qie", + "珈" : "jia", + "迦" : "jia", + "笳" : "jia", + "葭" : "jia", + "跏" : "jia", + "郏" : "jia", + "恝" : "jia", + "铗" : "jia", + "袷" : "qia", + "蛱" : "jia", + "角" : "jiao", + "挢" : "jiao", + "岬" : "jia", + "徼" : "jiao", + "湫" : "qiu", + "敫" : "jiao", + "瘕" : "jia", + "浅" : "qian", + "蒹" : "jian", + "搛" : "jian", + "湔" : "jian", + "缣" : "jian", + "犍" : "jian", + "鹣" : "jian", + "鲣" : "jian", + "鞯" : "jian", + "蹇" : "jian", + "謇" : "jian", + "硷" : "jian", + "枧" : "jian", + "戬" : "jian", + "谫" : "jian", + "囝" : "jian", + "裥" : "jian", + "笕" : "jian", + "翦" : "jian", + "趼" : "jian", + "楗" : "jian", + "牮" : "jian", + "踺" : "jian", + "茳" : "jiang", + "礓" : "jiang", + "耩" : "jiang", + "降" : "jiang", + "绛" : "jiang", + "洚" : "jiang", + "鲛" : "jiao", + "僬" : "jiao", + "鹪" : "jiao", + "艽" : "jiao", + "茭" : "jiao", + "嚼" : "jiao", + "峤" : "qiao", + "觉" : "jiao", + "校" : "xiao", + "噍" : "jiao", + "醮" : "jiao", + "疖" : "jie", + "喈" : "jie", + "桔" : "ju", + "拮" : "jie", + "桀" : "jie", + "颉" : "jie", + "婕" : "jie", + "羯" : "jie", + "鲒" : "jie", + "蚧" : "jie", + "骱" : "jie", + "衿" : "jin", + "馑" : "jin", + "卺" : "jin", + "廑" : "jin", + "堇" : "jin", + "槿" : "jin", + "靳" : "jin", + "缙" : "jin", + "荩" : "jin", + "赆" : "jin", + "妗" : "jin", + "旌" : "jing", + "腈" : "jing", + "憬" : "jing", + "肼" : "jing", + "迳" : "jing", + "胫" : "jing", + "弪" : "jing", + "獍" : "jing", + "扃" : "jiong", + "鬏" : "jiu", + "疚" : "jiu", + "僦" : "jiu", + "桕" : "jiu", + "疽" : "ju", + "裾" : "ju", + "苴" : "ju", + "椐" : "ju", + "锔" : "ju", + "琚" : "ju", + "鞫" : "ju", + "踽" : "ju", + "榉" : "ju", + "莒" : "ju", + "遽" : "ju", + "倨" : "ju", + "钜" : "ju", + "犋" : "ju", + "屦" : "ju", + "榘" : "ju", + "窭" : "ju", + "讵" : "ju", + "醵" : "ju", + "苣" : "ju", + "圈" : "quan", + "镌" : "juan", + "蠲" : "juan", + "锩" : "juan", + "狷" : "juan", + "桊" : "juan", + "鄄" : "juan", + "獗" : "jue", + "攫" : "jue", + "孓" : "jue", + "橛" : "jue", + "珏" : "jue", + "桷" : "jue", + "劂" : "jue", + "爝" : "jue", + "镢" : "jue", + "觖" : "jue", + "筠" : "jun", + "麇" : "jun", + "捃" : "jun", + "浚" : "jun", + "喀" : "ka", + "卡" : "ka", + "佧" : "ka", + "胩" : "ka", + "锎" : "kai", + "蒈" : "kai", + "剀" : "kai", + "垲" : "kai", + "锴" : "kai", + "戡" : "kan", + "莰" : "kan", + "闶" : "kang", + "钪" : "kang", + "尻" : "kao", + "栲" : "kao", + "柯" : "ke", + "疴" : "ke", + "钶" : "ke", + "颏" : "ke", + "珂" : "ke", + "髁" : "ke", + "壳" : "ke", + "岢" : "ke", + "溘" : "ke", + "骒" : "ke", + "缂" : "ke", + "氪" : "ke", + "锞" : "ke", + "裉" : "ken", + "倥" : "kong", + "崆" : "kong", + "箜" : "kong", + "芤" : "kou", + "眍" : "kou", + "筘" : "kou", + "刳" : "ku", + "堀" : "ku", + "喾" : "ku", + "侉" : "kua", + "蒯" : "kuai", + "哙" : "kuai", + "狯" : "kuai", + "郐" : "kuai", + "匡" : "kuang", + "夼" : "kuang", + "邝" : "kuang", + "圹" : "kuang", + "纩" : "kuang", + "贶" : "kuang", + "岿" : "kui", + "悝" : "kui", + "睽" : "kui", + "逵" : "kui", + "馗" : "kui", + "夔" : "kui", + "喹" : "kui", + "隗" : "wei", + "暌" : "kui", + "揆" : "kui", + "蝰" : "kui", + "跬" : "kui", + "喟" : "kui", + "聩" : "kui", + "篑" : "kui", + "蒉" : "kui", + "愦" : "kui", + "锟" : "kun", + "醌" : "kun", + "琨" : "kun", + "髡" : "kun", + "悃" : "kun", + "阃" : "kun", + "蛞" : "kuo", + "砬" : "la", + "落" : "luo", + "剌" : "la", + "瘌" : "la", + "涞" : "lai", + "崃" : "lai", + "铼" : "lai", + "赉" : "lai", + "濑" : "lai", + "斓" : "lan", + "镧" : "lan", + "谰" : "lan", + "漤" : "lan", + "罱" : "lan", + "稂" : "lang", + "阆" : "lang", + "莨" : "liang", + "蒗" : "lang", + "铹" : "lao", + "痨" : "lao", + "醪" : "lao", + "栳" : "lao", + "铑" : "lao", + "耢" : "lao", + "勒" : "le", + "仂" : "le", + "叻" : "le", + "泐" : "le", + "鳓" : "le", + "了" : "le", + "镭" : "lei", + "嫘" : "lei", + "缧" : "lei", + "檑" : "lei", + "诔" : "lei", + "耒" : "lei", + "酹" : "lei", + "塄" : "leng", + "愣" : "leng", + "藜" : "li", + "骊" : "li", + "黧" : "li", + "缡" : "li", + "嫠" : "li", + "鲡" : "li", + "蓠" : "li", + "澧" : "li", + "锂" : "li", + "醴" : "li", + "鳢" : "li", + "俪" : "li", + "砺" : "li", + "郦" : "li", + "詈" : "li", + "猁" : "li", + "溧" : "li", + "栎" : "li", + "轹" : "li", + "傈" : "li", + "坜" : "li", + "苈" : "li", + "疠" : "li", + "疬" : "li", + "篥" : "li", + "粝" : "li", + "跞" : "li", + "俩" : "liang", + "裢" : "lian", + "濂" : "lian", + "臁" : "lian", + "奁" : "lian", + "蠊" : "lian", + "琏" : "lian", + "蔹" : "lian", + "裣" : "lian", + "楝" : "lian", + "潋" : "lian", + "椋" : "liang", + "墚" : "liang", + "寮" : "liao", + "鹩" : "liao", + "蓼" : "liao", + "钌" : "liao", + "廖" : "liao", + "尥" : "liao", + "洌" : "lie", + "捩" : "lie", + "埒" : "lie", + "躐" : "lie", + "鬣" : "lie", + "辚" : "lin", + "遴" : "lin", + "啉" : "lin", + "瞵" : "lin", + "懔" : "lin", + "廪" : "lin", + "蔺" : "lin", + "膦" : "lin", + "酃" : "ling", + "柃" : "ling", + "鲮" : "ling", + "呤" : "ling", + "镏" : "liu", + "旒" : "liu", + "骝" : "liu", + "鎏" : "liu", + "锍" : "liu", + "碌" : "lu", + "鹨" : "liu", + "茏" : "long", + "栊" : "long", + "泷" : "long", + "砻" : "long", + "癃" : "long", + "垅" : "long", + "偻" : "lou", + "蝼" : "lou", + "蒌" : "lou", + "耧" : "lou", + "嵝" : "lou", + "露" : "lu", + "瘘" : "lou", + "噜" : "lu", + "轳" : "lu", + "垆" : "lu", + "胪" : "lu", + "舻" : "lu", + "栌" : "lu", + "镥" : "lu", + "绿" : "lv", + "辘" : "lu", + "簏" : "lu", + "潞" : "lu", + "辂" : "lu", + "渌" : "lu", + "氇" : "lu", + "捋" : "lv", + "稆" : "lv", + "率" : "lv", + "闾" : "lv", + "栾" : "luan", + "銮" : "luan", + "滦" : "luan", + "娈" : "luan", + "脔" : "luan", + "锊" : "lve", + "猡" : "luo", + "椤" : "luo", + "脶" : "luo", + "镙" : "luo", + "倮" : "luo", + "蠃" : "luo", + "瘰" : "luo", + "珞" : "luo", + "泺" : "luo", + "荦" : "luo", + "雒" : "luo", + "呒" : "mu", + "抹" : "mo", + "唛" : "mai", + "杩" : "ma", + "么" : "me", + "埋" : "mai", + "荬" : "mai", + "脉" : "mai", + "劢" : "mai", + "颟" : "man", + "蔓" : "man", + "鳗" : "man", + "鞔" : "man", + "螨" : "man", + "墁" : "man", + "缦" : "man", + "熳" : "man", + "镘" : "man", + "邙" : "mang", + "硭" : "mang", + "旄" : "mao", + "茆" : "mao", + "峁" : "mao", + "泖" : "mao", + "昴" : "mao", + "耄" : "mao", + "瑁" : "mao", + "懋" : "mao", + "瞀" : "mao", + "麽" : "me", + "没" : "mei", + "嵋" : "mei", + "湄" : "mei", + "猸" : "mei", + "镅" : "mei", + "鹛" : "mei", + "浼" : "mei", + "钔" : "men", + "瞢" : "meng", + "甍" : "meng", + "礞" : "meng", + "艨" : "meng", + "黾" : "mian", + "鳘" : "min", + "溟" : "ming", + "暝" : "ming", + "模" : "mo", + "谟" : "mo", + "嫫" : "mo", + "镆" : "mo", + "瘼" : "mo", + "耱" : "mo", + "貊" : "mo", + "貘" : "mo", + "牟" : "mou", + "鍪" : "mou", + "蛑" : "mou", + "侔" : "mou", + "毪" : "mu", + "坶" : "mu", + "仫" : "mu", + "唔" : "wu", + "那" : "na", + "镎" : "na", + "哪" : "na", + "呢" : "ne", + "肭" : "na", + "艿" : "nai", + "鼐" : "nai", + "萘" : "nai", + "柰" : "nai", + "蝻" : "nan", + "馕" : "nang", + "攮" : "nang", + "曩" : "nang", + "猱" : "nao", + "铙" : "nao", + "硇" : "nao", + "蛲" : "nao", + "垴" : "nao", + "坭" : "ni", + "猊" : "ni", + "铌" : "ni", + "鲵" : "ni", + "祢" : "mi", + "睨" : "ni", + "慝" : "te", + "伲" : "ni", + "鲇" : "nian", + "鲶" : "nian", + "埝" : "nian", + "嬲" : "niao", + "茑" : "niao", + "脲" : "niao", + "啮" : "nie", + "陧" : "nie", + "颞" : "nie", + "臬" : "nie", + "蘖" : "nie", + "甯" : "ning", + "聍" : "ning", + "狃" : "niu", + "侬" : "nong", + "耨" : "nou", + "孥" : "nu", + "胬" : "nu", + "钕" : "nv", + "恧" : "nv", + "褰" : "qian", + "掮" : "qian", + "荨" : "xun", + "钤" : "qian", + "箝" : "qian", + "鬈" : "quan", + "缱" : "qian", + "肷" : "qian", + "纤" : "xian", + "茜" : "qian", + "慊" : "qian", + "椠" : "qian", + "戗" : "qiang", + "镪" : "qiang", + "锖" : "qiang", + "樯" : "qiang", + "嫱" : "qiang", + "雀" : "que", + "缲" : "qiao", + "硗" : "qiao", + "劁" : "qiao", + "樵" : "qiao", + "谯" : "qiao", + "鞒" : "qiao", + "愀" : "qiao", + "鞘" : "qiao", + "郄" : "xi", + "箧" : "qie", + "亲" : "qin", + "覃" : "tan", + "溱" : "qin", + "檎" : "qin", + "锓" : "qin", + "嗪" : "qin", + "螓" : "qin", + "揿" : "qin", + "吣" : "qin", + "圊" : "qing", + "鲭" : "qing", + "檠" : "qing", + "黥" : "qing", + "謦" : "qing", + "苘" : "qing", + "磬" : "qing", + "箐" : "qing", + "綮" : "qi", + "茕" : "qiong", + "邛" : "dao", + "蛩" : "tun", + "筇" : "qiong", + "跫" : "qiong", + "銎" : "qiong", + "楸" : "qiu", + "俅" : "qiu", + "赇" : "qiu", + "逑" : "qiu", + "犰" : "qiu", + "蝤" : "qiu", + "巯" : "qiu", + "鼽" : "qiu", + "糗" : "qiu", + "区" : "qu", + "祛" : "qu", + "麴" : "qu", + "诎" : "qu", + "衢" : "qu", + "癯" : "qu", + "劬" : "qu", + "璩" : "qu", + "氍" : "qu", + "朐" : "qu", + "磲" : "qu", + "鸲" : "qu", + "蕖" : "qu", + "蠼" : "qu", + "蘧" : "qu", + "阒" : "qu", + "颧" : "quan", + "荃" : "quan", + "铨" : "quan", + "辁" : "quan", + "筌" : "quan", + "绻" : "quan", + "畎" : "quan", + "阕" : "que", + "悫" : "que", + "髯" : "ran", + "禳" : "rang", + "穰" : "rang", + "仞" : "ren", + "妊" : "ren", + "轫" : "ren", + "衽" : "ren", + "狨" : "rong", + "肜" : "rong", + "蝾" : "rong", + "嚅" : "ru", + "濡" : "ru", + "薷" : "ru", + "襦" : "ru", + "颥" : "ru", + "洳" : "ru", + "溽" : "ru", + "蓐" : "ru", + "朊" : "ruan", + "蕤" : "rui", + "枘" : "rui", + "箬" : "ruo", + "挲" : "suo", + "脎" : "sa", + "塞" : "sai", + "鳃" : "sai", + "噻" : "sai", + "毵" : "san", + "馓" : "san", + "糁" : "san", + "霰" : "xian", + "磉" : "sang", + "颡" : "sang", + "缫" : "sao", + "鳋" : "sao", + "埽" : "sao", + "瘙" : "sao", + "色" : "se", + "杉" : "shan", + "鲨" : "sha", + "痧" : "sha", + "裟" : "sha", + "铩" : "sha", + "唼" : "sha", + "酾" : "shai", + "栅" : "zha", + "跚" : "shan", + "芟" : "shan", + "埏" : "shan", + "钐" : "shan", + "舢" : "shan", + "剡" : "yan", + "鄯" : "shan", + "疝" : "shan", + "蟮" : "shan", + "墒" : "shang", + "垧" : "shang", + "绱" : "shang", + "蛸" : "shao", + "筲" : "shao", + "苕" : "tiao", + "召" : "zhao", + "劭" : "shao", + "猞" : "she", + "畲" : "she", + "折" : "zhe", + "滠" : "she", + "歙" : "xi", + "厍" : "she", + "莘" : "shen", + "娠" : "shen", + "诜" : "shen", + "什" : "shen", + "谂" : "shen", + "渖" : "shen", + "矧" : "shen", + "胂" : "shen", + "椹" : "shen", + "省" : "sheng", + "眚" : "sheng", + "嵊" : "sheng", + "嘘" : "xu", + "蓍" : "shi", + "鲺" : "shi", + "识" : "shi", + "拾" : "shi", + "埘" : "shi", + "莳" : "shi", + "炻" : "shi", + "鲥" : "shi", + "豕" : "shi", + "似" : "si", + "噬" : "shi", + "贳" : "shi", + "铈" : "shi", + "螫" : "shi", + "筮" : "shi", + "殖" : "zhi", + "熟" : "shu", + "艏" : "shou", + "菽" : "shu", + "摅" : "shu", + "纾" : "shu", + "毹" : "shu", + "疋" : "shu", + "数" : "shu", + "属" : "shu", + "术" : "shu", + "澍" : "shu", + "沭" : "shu", + "丨" : "shu", + "腧" : "shu", + "说" : "shuo", + "妁" : "shuo", + "蒴" : "shuo", + "槊" : "shuo", + "搠" : "shuo", + "鸶" : "si", + "澌" : "si", + "缌" : "si", + "锶" : "si", + "厶" : "si", + "蛳" : "si", + "驷" : "si", + "泗" : "si", + "汜" : "si", + "兕" : "si", + "姒" : "si", + "耜" : "si", + "笥" : "si", + "忪" : "song", + "淞" : "song", + "崧" : "song", + "凇" : "song", + "菘" : "song", + "竦" : "song", + "溲" : "sou", + "飕" : "sou", + "蜩" : "tiao", + "萜" : "tie", + "汀" : "ting", + "葶" : "ting", + "莛" : "ting", + "梃" : "ting", + "佟" : "tong", + "酮" : "tong", + "仝" : "tong", + "茼" : "tong", + "砼" : "tong", + "钭" : "dou", + "酴" : "tu", + "钍" : "tu", + "堍" : "tu", + "抟" : "tuan", + "忒" : "te", + "煺" : "tui", + "暾" : "tun", + "氽" : "tun", + "乇" : "tuo", + "砣" : "tuo", + "沱" : "tuo", + "跎" : "tuo", + "坨" : "tuo", + "橐" : "tuo", + "酡" : "tuo", + "鼍" : "tuo", + "庹" : "tuo", + "拓" : "tuo", + "柝" : "tuo", + "箨" : "tuo", + "腽" : "wa", + "崴" : "wai", + "芄" : "wan", + "畹" : "wan", + "琬" : "wan", + "脘" : "wan", + "菀" : "wan", + "尢" : "you", + "辋" : "wang", + "魍" : "wang", + "逶" : "wei", + "葳" : "wei", + "隈" : "wei", + "惟" : "wei", + "帏" : "wei", + "圩" : "wei", + "囗" : "wei", + "潍" : "wei", + "嵬" : "wei", + "沩" : "wei", + "涠" : "wei", + "尾" : "wei", + "玮" : "wei", + "炜" : "wei", + "韪" : "wei", + "洧" : "wei", + "艉" : "wei", + "鲔" : "wei", + "遗" : "yi", + "尉" : "wei", + "軎" : "wei", + "璺" : "wen", + "阌" : "wen", + "蓊" : "weng", + "蕹" : "weng", + "渥" : "wo", + "硪" : "wo", + "龌" : "wo", + "圬" : "wu", + "吾" : "wu", + "浯" : "wu", + "鼯" : "wu", + "牾" : "wu", + "迕" : "wu", + "庑" : "wu", + "痦" : "wu", + "芴" : "wu", + "杌" : "wu", + "焐" : "wu", + "阢" : "wu", + "婺" : "wu", + "鋈" : "wu", + "樨" : "xi", + "栖" : "qi", + "郗" : "xi", + "蹊" : "qi", + "淅" : "xi", + "熹" : "xi", + "浠" : "xi", + "僖" : "xi", + "穸" : "xi", + "螅" : "xi", + "菥" : "xi", + "舾" : "xi", + "矽" : "xi", + "粞" : "xi", + "硒" : "xi", + "醯" : "xi", + "欷" : "xi", + "鼷" : "xi", + "檄" : "xi", + "隰" : "xi", + "觋" : "xi", + "屣" : "xi", + "葸" : "xi", + "蓰" : "xi", + "铣" : "xi", + "饩" : "xi", + "阋" : "xi", + "禊" : "xi", + "舄" : "xi", + "狎" : "xia", + "硖" : "xia", + "柙" : "xia", + "暹" : "xian", + "莶" : "xian", + "祆" : "xian", + "籼" : "xian", + "跹" : "xian", + "鹇" : "xian", + "痫" : "xian", + "猃" : "xian", + "燹" : "xian", + "蚬" : "xian", + "筅" : "xian", + "冼" : "xian", + "岘" : "xian", + "骧" : "xiang", + "葙" : "xiang", + "芗" : "xiang", + "缃" : "xiang", + "庠" : "xiang", + "鲞" : "xiang", + "蟓" : "xiang", + "削" : "xue", + "枵" : "xiao", + "绡" : "xiao", + "筱" : "xiao", + "邪" : "xie", + "勰" : "xie", + "缬" : "xie", + "血" : "xue", + "榭" : "xie", + "瀣" : "xie", + "薤" : "xie", + "燮" : "xie", + "躞" : "xie", + "廨" : "xie", + "绁" : "xie", + "渫" : "xie", + "榍" : "xie", + "獬" : "xie", + "昕" : "xin", + "忻" : "xin", + "囟" : "xin", + "陉" : "jing", + "荥" : "ying", + "饧" : "tang", + "硎" : "xing", + "荇" : "xing", + "芎" : "xiong", + "馐" : "xiu", + "庥" : "xiu", + "鸺" : "xiu", + "貅" : "xiu", + "髹" : "xiu", + "宿" : "xiu", + "岫" : "xiu", + "溴" : "xiu", + "吁" : "xu", + "盱" : "xu", + "顼" : "xu", + "糈" : "xu", + "醑" : "xu", + "洫" : "xu", + "溆" : "xu", + "蓿" : "xu", + "萱" : "xuan", + "谖" : "xuan", + "儇" : "xuan", + "煊" : "xuan", + "痃" : "xuan", + "铉" : "xuan", + "泫" : "xuan", + "碹" : "xuan", + "楦" : "xuan", + "镟" : "xuan", + "踅" : "xue", + "泶" : "xue", + "鳕" : "xue", + "埙" : "xun", + "曛" : "xun", + "窨" : "xun", + "獯" : "xun", + "峋" : "xun", + "洵" : "xun", + "恂" : "xun", + "浔" : "xun", + "鲟" : "xun", + "蕈" : "xun", + "垭" : "ya", + "岈" : "ya", + "琊" : "ya", + "痖" : "ya", + "迓" : "ya", + "砑" : "ya", + "咽" : "yan", + "鄢" : "yan", + "菸" : "yan", + "崦" : "yan", + "铅" : "qian", + "芫" : "yuan", + "兖" : "yan", + "琰" : "yan", + "罨" : "yan", + "厣" : "yan", + "焱" : "yan", + "酽" : "yan", + "谳" : "yan", + "鞅" : "yang", + "炀" : "yang", + "蛘" : "yang", + "约" : "yue", + "珧" : "yao", + "轺" : "yao", + "繇" : "yao", + "鳐" : "yao", + "崾" : "yao", + "钥" : "yao", + "曜" : "yao", + "铘" : "ye", + "烨" : "ye", + "邺" : "ye", + "靥" : "ye", + "晔" : "ye", + "猗" : "yi", + "铱" : "yi", + "欹" : "qi", + "黟" : "yi", + "怡" : "yi", + "沂" : "yi", + "圯" : "yi", + "荑" : "yi", + "诒" : "yi", + "眙" : "yi", + "嶷" : "yi", + "钇" : "yi", + "舣" : "yi", + "酏" : "yi", + "熠" : "yi", + "弋" : "yi", + "懿" : "yi", + "镒" : "yi", + "峄" : "yi", + "怿" : "yi", + "悒" : "yi", + "佾" : "yi", + "殪" : "yi", + "挹" : "yi", + "埸" : "yi", + "劓" : "yi", + "镱" : "yi", + "瘗" : "yi", + "癔" : "yi", + "翊" : "yi", + "蜴" : "yi", + "氤" : "yin", + "堙" : "yin", + "洇" : "yin", + "鄞" : "yin", + "狺" : "yin", + "夤" : "yin", + "圻" : "qi", + "饮" : "yin", + "吲" : "yin", + "胤" : "yin", + "茚" : "yin", + "璎" : "ying", + "撄" : "ying", + "嬴" : "ying", + "滢" : "ying", + "潆" : "ying", + "蓥" : "ying", + "瘿" : "ying", + "郢" : "ying", + "媵" : "ying", + "邕" : "yong", + "镛" : "yong", + "墉" : "yong", + "慵" : "yong", + "痈" : "yong", + "鳙" : "yong", + "饔" : "yong", + "喁" : "yong", + "俑" : "yong", + "莸" : "you", + "猷" : "you", + "疣" : "you", + "蚰" : "you", + "蝣" : "you", + "莜" : "you", + "牖" : "you", + "铕" : "you", + "卣" : "you", + "宥" : "you", + "侑" : "you", + "蚴" : "you", + "釉" : "you", + "馀" : "yu", + "萸" : "yu", + "禺" : "yu", + "妤" : "yu", + "欤" : "yu", + "觎" : "yu", + "窬" : "yu", + "蝓" : "yu", + "嵛" : "yu", + "舁" : "yu", + "雩" : "yu", + "龉" : "yu", + "伛" : "yu", + "圉" : "yu", + "庾" : "yu", + "瘐" : "yu", + "窳" : "yu", + "俣" : "yu", + "毓" : "yu", + "峪" : "yu", + "煜" : "yu", + "燠" : "yu", + "蓣" : "yu", + "饫" : "yu", + "阈" : "yu", + "鬻" : "yu", + "聿" : "yu", + "钰" : "yu", + "鹆" : "yu", + "蜮" : "yu", + "眢" : "yuan", + "箢" : "yuan", + "员" : "yuan", + "沅" : "yuan", + "橼" : "yuan", + "塬" : "yuan", + "爰" : "yuan", + "螈" : "yuan", + "鼋" : "yuan", + "掾" : "yuan", + "垸" : "yuan", + "瑗" : "yuan", + "刖" : "yue", + "瀹" : "yue", + "樾" : "yue", + "龠" : "yue", + "氲" : "yun", + "昀" : "yun", + "郧" : "yun", + "狁" : "yun", + "郓" : "yun", + "韫" : "yun", + "恽" : "yun", + "扎" : "zha", + "拶" : "za", + "咋" : "za", + "仔" : "zai", + "昝" : "zan", + "瓒" : "zan", + "藏" : "zang", + "奘" : "zang", + "唣" : "zao", + "择" : "ze", + "迮" : "ze", + "赜" : "ze", + "笮" : "ze", + "箦" : "ze", + "舴" : "ze", + "昃" : "ze", + "缯" : "zeng", + "罾" : "zeng", + "齄" : "zha", + "柞" : "zha", + "痄" : "zha", + "瘵" : "zhai", + "旃" : "zhan", + "璋" : "zhang", + "漳" : "zhang", + "嫜" : "zhang", + "鄣" : "zhang", + "仉" : "zhang", + "幛" : "zhang", + "着" : "zhe", + "啁" : "zhou", + "爪" : "zhao", + "棹" : "zhao", + "笊" : "zhao", + "摺" : "zhe", + "磔" : "zhe", + "这" : "zhe", + "柘" : "zhe", + "桢" : "zhen", + "蓁" : "zhen", + "祯" : "zhen", + "浈" : "zhen", + "畛" : "zhen", + "轸" : "zhen", + "稹" : "zhen", + "圳" : "zhen", + "徵" : "zhi", + "钲" : "zheng", + "卮" : "zhi", + "胝" : "zhi", + "祗" : "zhi", + "摭" : "zhi", + "絷" : "zhi", + "埴" : "zhi", + "轵" : "zhi", + "黹" : "zhi", + "帙" : "zhi", + "轾" : "zhi", + "贽" : "zhi", + "陟" : "zhi", + "忮" : "zhi", + "彘" : "zhi", + "膣" : "zhi", + "鸷" : "zhi", + "骘" : "zhi", + "踬" : "zhi", + "郅" : "zhi", + "觯" : "zhi", + "锺" : "zhong", + "螽" : "zhong", + "舯" : "zhong", + "碡" : "zhou", + "绉" : "zhou", + "荮" : "zhou", + "籀" : "zhou", + "酎" : "zhou", + "洙" : "zhu", + "邾" : "zhu", + "潴" : "zhu", + "槠" : "zhu", + "橥" : "zhu", + "舳" : "zhu", + "瘃" : "zhu", + "渚" : "zhu", + "麈" : "zhu", + "箸" : "zhu", + "炷" : "zhu", + "杼" : "zhu", + "翥" : "zhu", + "疰" : "zhu", + "颛" : "zhuan", + "赚" : "zhuan", + "馔" : "zhuan", + "僮" : "tong", + "缒" : "zhui", + "肫" : "zhun", + "窀" : "zhun", + "涿" : "zhuo", + "倬" : "zhuo", + "濯" : "zhuo", + "诼" : "zhuo", + "禚" : "zhuo", + "浞" : "zhuo", + "谘" : "zi", + "淄" : "zi", + "髭" : "zi", + "孳" : "zi", + "粢" : "zi", + "趑" : "zi", + "觜" : "zui", + "缁" : "zi", + "鲻" : "zi", + "嵫" : "zi", + "笫" : "zi", + "耔" : "zi", + "腙" : "zong", + "偬" : "zong", + "诹" : "zou", + "陬" : "zou", + "鄹" : "zou", + "驺" : "zou", + "鲰" : "zou", + "菹" : "ju", + "镞" : "zu", + "躜" : "zuan", + "缵" : "zuan", + "蕞" : "zui", + "撙" : "zun", + "胙" : "zuo", + "阿" : "a", + "阿" : "e", + "柏" : "bai", + "蚌" : "beng", + "薄" : "bo", + "堡" : "bao", + "呗" : "bei", + "贲" : "ben", + "臂" : "bi", + "瘪" : "bie", + "槟" : "bin", + "剥" : "bo", + "伯" : "bo", + "卜" : "bu", + "参" : "can", + "嚓" : "ca", + "差" : "cha", + "孱" : "chan", + "绰" : "chuo", + "称" : "cheng", + "澄" : "cheng", + "大" : "da", + "单" : "dan", + "得" : "de", + "的" : "de", + "地" : "di", + "都" : "dou", + "读" : "du", + "度" : "du", + "蹲" : "dun", + "佛" : "fo", + "伽" : "jia", + "盖" : "gai", + "镐" : "hao", + "给" : "gei", + "呱" : "gua", + "氿" : "jiu", + "桧" : "hui", + "掴" : "guo", + "蛤" : "ha", + "还" : "hai", + "和" : "he", + "核" : "he", + "哼" : "heng", + "鹄" : "hu", + "划" : "hua", + "夹" : "jia", + "贾" : "jia", + "芥" : "jie", + "劲" : "jin", + "荆" : "jing", + "颈" : "jing", + "貉" : "he", + "吖" : "a", + "啊" : "a", + "锕" : "a", + "哎" : "ai", + "哀" : "ai", + "埃" : "ai", + "唉" : "ai", + "欸" : "ai", + "锿" : "ai", + "挨" : "ai", + "皑" : "ai", + "癌" : "ai", + "毐" : "ai", + "矮" : "ai", + "蔼" : "ai", + "霭" : "ai", + "砹" : "ai", + "爱" : "ai", + "隘" : "ai", + "碍" : "ai", + "嗳" : "ai", + "嫒" : "ai", + "叆" : "ai", + "暧" : "ai", + "安" : "an", + "桉" : "an", + "氨" : "an", + "庵" : "an", + "谙" : "an", + "鹌" : "an", + "鞍" : "an", + "俺" : "an", + "埯" : "an", + "唵" : "an", + "铵" : "an", + "揞" : "an", + "岸" : "an", + "按" : "an", + "胺" : "an", + "案" : "an", + "暗" : "an", + "黯" : "an", + "玵" : "an", + "肮" : "ang", + "昂" : "ang", + "盎" : "ang", + "凹" : "ao", + "敖" : "ao", + "遨" : "ao", + "嗷" : "ao", + "獒" : "ao", + "熬" : "ao", + "聱" : "ao", + "螯" : "ao", + "翱" : "ao", + "謷" : "ao", + "鏖" : "ao", + "袄" : "ao", + "媪" : "ao", + "坳" : "ao", + "傲" : "ao", + "奥" : "ao", + "骜" : "ao", + "澳" : "ao", + "懊" : "ao", + "八" : "ba", + "巴" : "ba", + "叭" : "ba", + "芭" : "ba", + "疤" : "ba", + "捌" : "ba", + "笆" : "ba", + "粑" : "ba", + "拔" : "ba", + "茇" : "ba", + "妭" : "ba", + "菝" : "ba", + "跋" : "ba", + "魃" : "ba", + "把" : "ba", + "靶" : "ba", + "坝" : "ba", + "爸" : "ba", + "罢" : "ba", + "霸" : "ba", + "灞" : "ba", + "吧" : "ba", + "钯" : "ba", + "掰" : "bai", + "白" : "bai", + "百" : "bai", + "佰" : "bai", + "捭" : "bai", + "摆" : "bai", + "败" : "bai", + "拜" : "bai", + "稗" : "bai", + "扳" : "ban", + "攽" : "ban", + "班" : "ban", + "般" : "ban", + "颁" : "ban", + "斑" : "ban", + "搬" : "ban", + "瘢" : "ban", + "阪" : "ban", + "坂" : "ban", + "板" : "ban", + "版" : "ban", + "钣" : "ban", + "舨" : "ban", + "办" : "ban", + "半" : "ban", + "伴" : "ban", + "拌" : "ban", + "绊" : "ban", + "瓣" : "ban", + "扮" : "ban", + "邦" : "bang", + "帮" : "bang", + "梆" : "bang", + "浜" : "bang", + "绑" : "bang", + "榜" : "bang", + "棒" : "bang", + "傍" : "bang", + "谤" : "bang", + "蒡" : "bang", + "镑" : "bang", + "包" : "bao", + "苞" : "bao", + "孢" : "bao", + "胞" : "bao", + "龅" : "bao", + "煲" : "bao", + "褒" : "bao", + "雹" : "bao", + "饱" : "bao", + "宝" : "bao", + "保" : "bao", + "鸨" : "bao", + "葆" : "bao", + "褓" : "bao", + "报" : "bao", + "抱" : "bao", + "趵" : "bao", + "豹" : "bao", + "鲍" : "bao", + "暴" : "bao", + "爆" : "bao", + "枹" : "bao", + "杯" : "bei", + "卑" : "bei", + "悲" : "bei", + "碑" : "bei", + "北" : "bei", + "贝" : "bei", + "狈" : "bei", + "备" : "bei", + "背" : "bei", + "钡" : "bei", + "倍" : "bei", + "悖" : "bei", + "被" : "bei", + "辈" : "bei", + "惫" : "bei", + "焙" : "bei", + "蓓" : "bei", + "碚" : "bei", + "褙" : "bei", + "别" : "bei", + "蹩" : "bei", + "椑" : "bei", + "奔" : "ben", + "倴" : "ben", + "犇" : "ben", + "锛" : "ben", + "本" : "ben", + "苯" : "ben", + "坌" : "ben", + "笨" : "ben", + "崩" : "beng", + "绷" : "beng", + "嘣" : "beng", + "甭" : "beng", + "泵" : "beng", + "迸" : "beng", + "镚" : "beng", + "蹦" : "beng", + "屄" : "bi", + "逼" : "bi", + "荸" : "bi", + "鼻" : "bi", + "匕" : "bi", + "比" : "bi", + "吡" : "bi", + "沘" : "bi", + "妣" : "bi", + "彼" : "bi", + "秕" : "bi", + "笔" : "bi", + "俾" : "bi", + "鄙" : "bi", + "币" : "bi", + "必" : "bi", + "毕" : "bi", + "闭" : "bi", + "庇" : "bi", + "诐" : "bi", + "苾" : "bi", + "荜" : "bi", + "毖" : "bi", + "哔" : "bi", + "陛" : "bi", + "毙" : "bi", + "铋" : "bi", + "狴" : "bi", + "萆" : "bi", + "梐" : "bi", + "敝" : "bi", + "婢" : "bi", + "赑" : "bi", + "愎" : "bi", + "弼" : "bi", + "蓖" : "bi", + "痹" : "bi", + "滗" : "bi", + "碧" : "bi", + "蔽" : "bi", + "馝" : "bi", + "弊" : "bi", + "薜" : "bi", + "篦" : "bi", + "壁" : "bi", + "避" : "bi", + "髀" : "bi", + "璧" : "bi", + "芘" : "bi", + "边" : "bian", + "砭" : "bian", + "萹" : "bian", + "编" : "bian", + "煸" : "bian", + "蝙" : "bian", + "鳊" : "bian", + "鞭" : "bian", + "贬" : "bian", + "匾" : "bian", + "褊" : "bian", + "藊" : "bian", + "卞" : "bian", + "抃" : "bian", + "苄" : "bian", + "汴" : "bian", + "忭" : "bian", + "变" : "bian", + "遍" : "bian", + "辨" : "bian", + "辩" : "bian", + "辫" : "bian", + "标" : "biao", + "骉" : "biao", + "彪" : "biao", + "摽" : "biao", + "膘" : "biao", + "飙" : "biao", + "镖" : "biao", + "瀌" : "biao", + "镳" : "biao", + "表" : "biao", + "婊" : "biao", + "裱" : "biao", + "鳔" : "biao", + "憋" : "bie", + "鳖" : "bie", + "宾" : "bin", + "彬" : "bin", + "傧" : "bin", + "滨" : "bin", + "缤" : "bin", + "濒" : "bin", + "摈" : "bin", + "殡" : "bin", + "髌" : "bin", + "鬓" : "bin", + "冰" : "bing", + "兵" : "bing", + "丙" : "bing", + "邴" : "bing", + "秉" : "bing", + "柄" : "bing", + "饼" : "bing", + "炳" : "bing", + "禀" : "bing", + "并" : "bing", + "病" : "bing", + "摒" : "bing", + "拨" : "bo", + "波" : "bo", + "玻" : "bo", + "钵" : "bo", + "饽" : "bo", + "袯" : "bo", + "菠" : "bo", + "播" : "bo", + "驳" : "bo", + "帛" : "bo", + "勃" : "bo", + "钹" : "bo", + "铂" : "bo", + "亳" : "bo", + "舶" : "bo", + "脖" : "bo", + "博" : "bo", + "鹁" : "bo", + "渤" : "bo", + "搏" : "bo", + "馎" : "bo", + "箔" : "bo", + "膊" : "bo", + "踣" : "bo", + "馞" : "bo", + "礴" : "bo", + "跛" : "bo", + "檗" : "bo", + "擘" : "bo", + "簸" : "bo", + "啵" : "bo", + "蕃" : "bo", + "哱" : "bo", + "卟" : "bu", + "补" : "bu", + "捕" : "bu", + "哺" : "bu", + "不" : "bu", + "布" : "bu", + "步" : "bu", + "怖" : "bu", + "钚" : "bu", + "部" : "bu", + "埠" : "bu", + "簿" : "bu", + "擦" : "ca", + "猜" : "cai", + "才" : "cai", + "材" : "cai", + "财" : "cai", + "裁" : "cai", + "采" : "cai", + "彩" : "cai", + "睬" : "cai", + "踩" : "cai", + "菜" : "cai", + "蔡" : "cai", + "餐" : "can", + "残" : "can", + "蚕" : "can", + "惭" : "can", + "惨" : "can", + "黪" : "can", + "灿" : "can", + "粲" : "can", + "璨" : "can", + "穇" : "can", + "仓" : "cang", + "伧" : "cang", + "苍" : "cang", + "沧" : "cang", + "舱" : "cang", + "操" : "cao", + "糙" : "cao", + "曹" : "cao", + "嘈" : "cao", + "漕" : "cao", + "槽" : "cao", + "螬" : "cao", + "草" : "cao", + "册" : "ce", + "厕" : "ce", + "测" : "ce", + "恻" : "ce", + "策" : "ce", + "岑" : "cen", + "涔" : "cen", + "噌" : "ceng", + "层" : "ceng", + "嶒" : "ceng", + "蹭" : "ceng", + "叉" : "cha", + "杈" : "cha", + "插" : "cha", + "馇" : "cha", + "锸" : "cha", + "茬" : "cha", + "茶" : "cha", + "搽" : "cha", + "嵖" : "cha", + "猹" : "cha", + "槎" : "cha", + "碴" : "cha", + "察" : "cha", + "檫" : "cha", + "衩" : "cha", + "镲" : "cha", + "汊" : "cha", + "岔" : "cha", + "侘" : "cha", + "诧" : "cha", + "姹" : "cha", + "蹅" : "cha", + "拆" : "chai", + "钗" : "chai", + "侪" : "chai", + "柴" : "chai", + "豺" : "chai", + "虿" : "chai", + "茝" : "chai", + "觇" : "chan", + "掺" : "chan", + "搀" : "chan", + "襜" : "chan", + "谗" : "chan", + "婵" : "chan", + "馋" : "chan", + "缠" : "chan", + "蝉" : "chan", + "潺" : "chan", + "蟾" : "chan", + "巉" : "chan", + "产" : "chan", + "浐" : "chan", + "谄" : "chan", + "铲" : "chan", + "阐" : "chan", + "蒇" : "chan", + "骣" : "chan", + "冁" : "chan", + "忏" : "chan", + "颤" : "chan", + "羼" : "chan", + "韂" : "chan", + "伥" : "chang", + "昌" : "chang", + "菖" : "chang", + "猖" : "chang", + "娼" : "chang", + "肠" : "chang", + "尝" : "chang", + "常" : "chang", + "偿" : "chang", + "徜" : "chang", + "嫦" : "chang", + "厂" : "chang", + "场" : "chang", + "昶" : "chang", + "惝" : "chang", + "敞" : "chang", + "怅" : "chang", + "畅" : "chang", + "倡" : "chang", + "唱" : "chang", + "裳" : "chang", + "抄" : "chao", + "怊" : "chao", + "钞" : "chao", + "超" : "chao", + "晁" : "chao", + "巢" : "chao", + "嘲" : "chao", + "潮" : "chao", + "吵" : "chao", + "炒" : "chao", + "耖" : "chao", + "砗" : "che", + "扯" : "che", + "彻" : "che", + "坼" : "che", + "掣" : "che", + "撤" : "che", + "澈" : "che", + "瞮" : "che", + "抻" : "chen", + "郴" : "chen", + "嗔" : "chen", + "瞋" : "chen", + "臣" : "chen", + "尘" : "chen", + "辰" : "chen", + "沉" : "chen", + "忱" : "chen", + "陈" : "chen", + "宸" : "chen", + "晨" : "chen", + "谌" : "chen", + "碜" : "chen", + "衬" : "chen", + "龀" : "chen", + "趁" : "chen", + "柽" : "cheng", + "琤" : "cheng", + "撑" : "cheng", + "瞠" : "cheng", + "成" : "cheng", + "丞" : "cheng", + "呈" : "cheng", + "诚" : "cheng", + "承" : "cheng", + "城" : "cheng", + "铖" : "cheng", + "程" : "cheng", + "惩" : "cheng", + "酲" : "cheng", + "橙" : "cheng", + "逞" : "cheng", + "骋" : "cheng", + "秤" : "cheng", + "铛" : "cheng", + "樘" : "cheng", + "吃" : "chi", + "哧" : "chi", + "鸱" : "chi", + "蚩" : "chi", + "笞" : "chi", + "嗤" : "chi", + "痴" : "chi", + "媸" : "chi", + "魑" : "chi", + "池" : "chi", + "弛" : "chi", + "驰" : "chi", + "迟" : "chi", + "茌" : "chi", + "持" : "chi", + "踟" : "chi", + "尺" : "chi", + "齿" : "chi", + "侈" : "chi", + "耻" : "chi", + "豉" : "chi", + "褫" : "chi", + "彳" : "chi", + "叱" : "chi", + "斥" : "chi", + "赤" : "chi", + "饬" : "chi", + "炽" : "chi", + "翅" : "chi", + "敕" : "chi", + "啻" : "chi", + "傺" : "chi", + "匙" : "chi", + "冲" : "chong", + "充" : "chong", + "忡" : "chong", + "茺" : "chong", + "舂" : "chong", + "憧" : "chong", + "艟" : "chong", + "虫" : "chong", + "崇" : "chong", + "宠" : "chong", + "铳" : "chong", + "抽" : "chou", + "瘳" : "chou", + "惆" : "chou", + "绸" : "chou", + "畴" : "chou", + "酬" : "chou", + "稠" : "chou", + "愁" : "chou", + "筹" : "chou", + "踌" : "chou", + "丑" : "chou", + "瞅" : "chou", + "出" : "chu", + "初" : "chu", + "樗" : "chu", + "刍" : "chu", + "除" : "chu", + "厨" : "chu", + "锄" : "chu", + "滁" : "chu", + "蜍" : "chu", + "雏" : "chu", + "橱" : "chu", + "躇" : "chu", + "蹰" : "chu", + "杵" : "chu", + "础" : "chu", + "储" : "chu", + "楚" : "chu", + "褚" : "chu", + "亍" : "chu", + "处" : "chu", + "怵" : "chu", + "绌" : "chu", + "搐" : "chu", + "触" : "chu", + "憷" : "chu", + "黜" : "chu", + "矗" : "chu", + "揣" : "chuai", + "搋" : "chuai", + "膗" : "chuai", + "踹" : "chuai", + "川" : "chuan", + "氚" : "chuan", + "穿" : "chuan", + "舡" : "chuan", + "船" : "chuan", + "遄" : "chuan", + "椽" : "chuan", + "舛" : "chuan", + "喘" : "chuan", + "串" : "chuan", + "钏" : "chuan", + "疮" : "chuang", + "窗" : "chuang", + "床" : "chuang", + "闯" : "chuang", + "创" : "chuang", + "怆" : "chuang", + "吹" : "chui", + "炊" : "chui", + "垂" : "chui", + "陲" : "chui", + "捶" : "chui", + "棰" : "chui", + "槌" : "chui", + "锤" : "chui", + "春" : "chun", + "瑃" : "chun", + "椿" : "chun", + "蝽" : "chun", + "纯" : "chun", + "莼" : "chun", + "唇" : "chun", + "淳" : "chun", + "鹑" : "chun", + "醇" : "chun", + "蠢" : "chun", + "踔" : "chuo", + "戳" : "chuo", + "啜" : "chuo", + "惙" : "chuo", + "辍" : "chuo", + "龊" : "chuo", + "歠" : "chuo", + "疵" : "ci", + "词" : "ci", + "茈" : "ci", + "茨" : "ci", + "祠" : "ci", + "瓷" : "ci", + "辞" : "ci", + "慈" : "ci", + "磁" : "ci", + "雌" : "ci", + "鹚" : "ci", + "糍" : "ci", + "此" : "ci", + "泚" : "ci", + "跐" : "ci", + "次" : "ci", + "刺" : "ci", + "佽" : "ci", + "赐" : "ci", + "匆" : "cong", + "苁" : "cong", + "囱" : "cong", + "枞" : "cong", + "葱" : "cong", + "骢" : "cong", + "聪" : "cong", + "从" : "cong", + "丛" : "cong", + "淙" : "cong", + "悰" : "cong", + "琮" : "cong", + "凑" : "cou", + "辏" : "cou", + "腠" : "cou", + "粗" : "cu", + "徂" : "cu", + "殂" : "cu", + "促" : "cu", + "猝" : "cu", + "蔟" : "cu", + "醋" : "cu", + "踧" : "cu", + "簇" : "cu", + "蹙" : "cu", + "蹴" : "cu", + "汆" : "cuan", + "撺" : "cuan", + "镩" : "cuan", + "蹿" : "cuan", + "窜" : "cuan", + "篡" : "cuan", + "崔" : "cui", + "催" : "cui", + "摧" : "cui", + "璀" : "cui", + "脆" : "cui", + "萃" : "cui", + "啐" : "cui", + "淬" : "cui", + "悴" : "cui", + "毳" : "cui", + "瘁" : "cui", + "粹" : "cui", + "翠" : "cui", + "村" : "cun", + "皴" : "cun", + "存" : "cun", + "忖" : "cun", + "寸" : "cun", + "吋" : "cun", + "搓" : "cuo", + "磋" : "cuo", + "蹉" : "cuo", + "嵯" : "cuo", + "矬" : "cuo", + "痤" : "cuo", + "脞" : "cuo", + "挫" : "cuo", + "莝" : "cuo", + "厝" : "cuo", + "措" : "cuo", + "锉" : "cuo", + "错" : "cuo", + "酇" : "cuo", + "咑" : "da", + "垯" : "da", + "耷" : "da", + "搭" : "da", + "褡" : "da", + "达" : "da", + "怛" : "da", + "妲" : "da", + "荙" : "da", + "笪" : "da", + "答" : "da", + "跶" : "da", + "靼" : "da", + "瘩" : "da", + "鞑" : "da", + "打" : "da", + "呆" : "dai", + "歹" : "dai", + "逮" : "dai", + "傣" : "dai", + "代" : "dai", + "岱" : "dai", + "迨" : "dai", + "玳" : "dai", + "带" : "dai", + "殆" : "dai", + "贷" : "dai", + "待" : "dai", + "怠" : "dai", + "袋" : "dai", + "叇" : "dai", + "戴" : "dai", + "黛" : "dai", + "襶" : "dai", + "呔" : "dai", + "丹" : "dan", + "担" : "dan", + "眈" : "dan", + "耽" : "dan", + "郸" : "dan", + "聃" : "dan", + "殚" : "dan", + "瘅" : "dan", + "箪" : "dan", + "儋" : "dan", + "胆" : "dan", + "疸" : "dan", + "掸" : "dan", + "亶" : "dan", + "旦" : "dan", + "但" : "dan", + "诞" : "dan", + "萏" : "dan", + "啖" : "dan", + "淡" : "dan", + "惮" : "dan", + "蛋" : "dan", + "氮" : "dan", + "赕" : "dan", + "当" : "dang", + "裆" : "dang", + "挡" : "dang", + "档" : "dang", + "党" : "dang", + "谠" : "dang", + "凼" : "dang", + "砀" : "dang", + "宕" : "dang", + "荡" : "dang", + "菪" : "dang", + "刀" : "dao", + "忉" : "dao", + "氘" : "dao", + "舠" : "dao", + "导" : "dao", + "岛" : "dao", + "捣" : "dao", + "倒" : "dao", + "捯" : "dao", + "祷" : "dao", + "蹈" : "dao", + "到" : "dao", + "盗" : "dao", + "悼" : "dao", + "道" : "dao", + "稻" : "dao", + "焘" : "dao", + "锝" : "de", + "嘚" : "de", + "德" : "de", + "扽" : "den", + "灯" : "deng", + "登" : "deng", + "噔" : "deng", + "蹬" : "deng", + "等" : "deng", + "戥" : "deng", + "邓" : "deng", + "僜" : "deng", + "凳" : "deng", + "嶝" : "deng", + "磴" : "deng", + "瞪" : "deng", + "镫" : "deng", + "低" : "di", + "羝" : "di", + "堤" : "di", + "嘀" : "di", + "滴" : "di", + "狄" : "di", + "迪" : "di", + "籴" : "di", + "荻" : "di", + "敌" : "di", + "涤" : "di", + "笛" : "di", + "觌" : "di", + "嫡" : "di", + "镝" : "di", + "氐" : "di", + "邸" : "di", + "诋" : "di", + "抵" : "di", + "底" : "di", + "柢" : "di", + "砥" : "di", + "骶" : "di", + "玓" : "di", + "弟" : "di", + "帝" : "di", + "递" : "di", + "娣" : "di", + "第" : "di", + "谛" : "di", + "蒂" : "di", + "棣" : "di", + "睇" : "di", + "缔" : "di", + "碲" : "di", + "嗲" : "dia", + "掂" : "dian", + "滇" : "dian", + "颠" : "dian", + "巅" : "dian", + "癫" : "dian", + "典" : "dian", + "点" : "dian", + "碘" : "dian", + "踮" : "dian", + "电" : "dian", + "甸" : "dian", + "阽" : "dian", + "坫" : "dian", + "店" : "dian", + "玷" : "dian", + "垫" : "dian", + "钿" : "dian", + "淀" : "dian", + "惦" : "dian", + "奠" : "dian", + "殿" : "dian", + "靛" : "dian", + "刁" : "diao", + "叼" : "diao", + "汈" : "diao", + "凋" : "diao", + "貂" : "diao", + "碉" : "diao", + "雕" : "diao", + "鲷" : "diao", + "屌" : "diao", + "吊" : "diao", + "钓" : "diao", + "窎" : "diao", + "掉" : "diao", + "铫" : "diao", + "爹" : "die", + "跌" : "die", + "迭" : "die", + "谍" : "die", + "耋" : "die", + "喋" : "die", + "牒" : "die", + "叠" : "die", + "碟" : "die", + "嵽" : "die", + "蝶" : "die", + "蹀" : "die", + "鲽" : "die", + "仃" : "ding", + "叮" : "ding", + "玎" : "ding", + "盯" : "ding", + "町" : "ding", + "耵" : "ding", + "顶" : "ding", + "酊" : "ding", + "鼎" : "ding", + "订" : "ding", + "钉" : "ding", + "定" : "ding", + "啶" : "ding", + "腚" : "ding", + "碇" : "ding", + "锭" : "ding", + "丢" : "diu", + "铥" : "diu", + "东" : "dong", + "冬" : "dong", + "咚" : "dong", + "氡" : "dong", + "鸫" : "dong", + "董" : "dong", + "懂" : "dong", + "动" : "dong", + "冻" : "dong", + "侗" : "dong", + "栋" : "dong", + "胨" : "dong", + "洞" : "dong", + "胴" : "dong", + "兜" : "dou", + "蔸" : "dou", + "篼" : "dou", + "抖" : "dou", + "陡" : "dou", + "蚪" : "dou", + "斗" : "dou", + "豆" : "dou", + "逗" : "dou", + "痘" : "dou", + "窦" : "dou", + "督" : "du", + "嘟" : "du", + "毒" : "du", + "独" : "du", + "渎" : "du", + "椟" : "du", + "犊" : "du", + "牍" : "du", + "黩" : "du", + "髑" : "du", + "厾" : "du", + "笃" : "du", + "堵" : "du", + "赌" : "du", + "睹" : "du", + "杜" : "du", + "肚" : "du", + "妒" : "du", + "渡" : "du", + "镀" : "du", + "蠹" : "du", + "端" : "duan", + "短" : "duan", + "段" : "duan", + "断" : "duan", + "缎" : "duan", + "椴" : "duan", + "锻" : "duan", + "簖" : "duan", + "堆" : "dui", + "队" : "dui", + "对" : "dui", + "兑" : "dui", + "怼" : "dui", + "憝" : "dui", + "吨" : "dun", + "惇" : "dun", + "敦" : "dun", + "墩" : "dun", + "礅" : "dun", + "盹" : "dun", + "趸" : "dun", + "沌" : "dun", + "炖" : "dun", + "砘" : "dun", + "钝" : "dun", + "盾" : "dun", + "顿" : "dun", + "遁" : "dun", + "多" : "duo", + "咄" : "duo", + "哆" : "duo", + "掇" : "duo", + "裰" : "duo", + "夺" : "duo", + "踱" : "duo", + "朵" : "duo", + "垛" : "duo", + "哚" : "duo", + "躲" : "duo", + "亸" : "duo", + "剁" : "duo", + "舵" : "duo", + "堕" : "duo", + "惰" : "duo", + "跺" : "duo", + "屙" : "e", + "婀" : "e", + "讹" : "e", + "囮" : "e", + "俄" : "e", + "莪" : "e", + "峨" : "e", + "娥" : "e", + "锇" : "e", + "鹅" : "e", + "蛾" : "e", + "额" : "e", + "厄" : "e", + "扼" : "e", + "苊" : "e", + "呃" : "e", + "垩" : "e", + "饿" : "e", + "鄂" : "e", + "谔" : "e", + "萼" : "e", + "遏" : "e", + "愕" : "e", + "腭" : "e", + "颚" : "e", + "噩" : "e", + "鳄" : "e", + "恩" : "en", + "蒽" : "en", + "摁" : "en", + "鞥" : "eng", + "儿" : "er", + "而" : "er", + "鸸" : "er", + "尔" : "er", + "耳" : "er", + "迩" : "er", + "饵" : "er", + "洱" : "er", + "铒" : "er", + "二" : "er", + "贰" : "er", + "发" : "fa", + "乏" : "fa", + "伐" : "fa", + "罚" : "fa", + "垡" : "fa", + "阀" : "fa", + "筏" : "fa", + "法" : "fa", + "砝" : "fa", + "珐" : "fa", + "帆" : "fan", + "幡" : "fan", + "藩" : "fan", + "翻" : "fan", + "凡" : "fan", + "矾" : "fan", + "钒" : "fan", + "烦" : "fan", + "樊" : "fan", + "燔" : "fan", + "繁" : "fan", + "蹯" : "fan", + "蘩" : "fan", + "反" : "fan", + "返" : "fan", + "犯" : "fan", + "饭" : "fan", + "泛" : "fan", + "范" : "fan", + "贩" : "fan", + "畈" : "fan", + "梵" : "fan", + "方" : "fang", + "邡" : "fang", + "坊" : "fang", + "芳" : "fang", + "枋" : "fang", + "钫" : "fang", + "防" : "fang", + "妨" : "fang", + "肪" : "fang", + "房" : "fang", + "鲂" : "fang", + "仿" : "fang", + "访" : "fang", + "纺" : "fang", + "舫" : "fang", + "放" : "fang", + "飞" : "fei", + "妃" : "fei", + "非" : "fei", + "菲" : "fei", + "啡" : "fei", + "绯" : "fei", + "扉" : "fei", + "肥" : "fei", + "淝" : "fei", + "腓" : "fei", + "匪" : "fei", + "诽" : "fei", + "悱" : "fei", + "棐" : "fei", + "斐" : "fei", + "榧" : "fei", + "翡" : "fei", + "篚" : "fei", + "吠" : "fei", + "肺" : "fei", + "狒" : "fei", + "废" : "fei", + "沸" : "fei", + "费" : "fei", + "痱" : "fei", + "镄" : "fei", + "分" : "fen", + "芬" : "fen", + "吩" : "fen", + "纷" : "fen", + "氛" : "fen", + "酚" : "fen", + "坟" : "fen", + "汾" : "fen", + "棼" : "fen", + "焚" : "fen", + "鼢" : "fen", + "粉" : "fen", + "份" : "fen", + "奋" : "fen", + "忿" : "fen", + "偾" : "fen", + "粪" : "fen", + "愤" : "fen", + "丰" : "feng", + "风" : "feng", + "沣" : "feng", + "枫" : "feng", + "封" : "feng", + "砜" : "feng", + "疯" : "feng", + "峰" : "feng", + "烽" : "feng", + "葑" : "feng", + "锋" : "feng", + "蜂" : "feng", + "酆" : "feng", + "冯" : "feng", + "逢" : "feng", + "缝" : "feng", + "讽" : "feng", + "唪" : "feng", + "凤" : "feng", + "奉" : "feng", + "俸" : "feng", + "缶" : "fou", + "夫" : "fu", + "呋" : "fu", + "肤" : "fu", + "麸" : "fu", + "跗" : "fu", + "稃" : "fu", + "孵" : "fu", + "敷" : "fu", + "弗" : "fu", + "伏" : "fu", + "凫" : "fu", + "扶" : "fu", + "芙" : "fu", + "孚" : "fu", + "拂" : "fu", + "苻" : "fu", + "服" : "fu", + "怫" : "fu", + "茯" : "fu", + "氟" : "fu", + "俘" : "fu", + "浮" : "fu", + "符" : "fu", + "匐" : "fu", + "涪" : "fu", + "艴" : "fu", + "幅" : "fu", + "辐" : "fu", + "蜉" : "fu", + "福" : "fu", + "蝠" : "fu", + "抚" : "fu", + "甫" : "fu", + "拊" : "fu", + "斧" : "fu", + "府" : "fu", + "俯" : "fu", + "釜" : "fu", + "辅" : "fu", + "腑" : "fu", + "腐" : "fu", + "父" : "fu", + "讣" : "fu", + "付" : "fu", + "负" : "fu", + "妇" : "fu", + "附" : "fu", + "咐" : "fu", + "阜" : "fu", + "驸" : "fu", + "赴" : "fu", + "复" : "fu", + "副" : "fu", + "赋" : "fu", + "傅" : "fu", + "富" : "fu", + "腹" : "fu", + "缚" : "fu", + "赙" : "fu", + "蝮" : "fu", + "覆" : "fu", + "馥" : "fu", + "袱" : "fu", + "旮" : "ga", + "嘎" : "ga", + "钆" : "ga", + "尜" : "ga", + "尕" : "ga", + "尬" : "ga", + "该" : "gai", + "垓" : "gai", + "荄" : "gai", + "赅" : "gai", + "改" : "gai", + "丐" : "gai", + "钙" : "gai", + "溉" : "gai", + "概" : "gai", + "甘" : "gan", + "玕" : "gan", + "肝" : "gan", + "坩" : "gan", + "苷" : "gan", + "矸" : "gan", + "泔" : "gan", + "柑" : "gan", + "竿" : "gan", + "酐" : "gan", + "疳" : "gan", + "尴" : "gan", + "杆" : "gan", + "秆" : "gan", + "赶" : "gan", + "敢" : "gan", + "感" : "gan", + "澉" : "gan", + "橄" : "gan", + "擀" : "gan", + "干" : "gan", + "旰" : "gan", + "绀" : "gan", + "淦" : "gan", + "骭" : "gan", + "赣" : "gan", + "冈" : "gang", + "冮" : "gang", + "刚" : "gang", + "肛" : "gang", + "纲" : "gang", + "钢" : "gang", + "缸" : "gang", + "罡" : "gang", + "岗" : "gang", + "港" : "gang", + "杠" : "gang", + "皋" : "gao", + "高" : "gao", + "羔" : "gao", + "睾" : "gao", + "膏" : "gao", + "篙" : "gao", + "糕" : "gao", + "杲" : "gao", + "搞" : "gao", + "槁" : "gao", + "稿" : "gao", + "告" : "gao", + "郜" : "gao", + "诰" : "gao", + "锆" : "gao", + "戈" : "ge", + "圪" : "ge", + "纥" : "ge", + "疙" : "ge", + "哥" : "ge", + "胳" : "ge", + "鸽" : "ge", + "袼" : "ge", + "搁" : "ge", + "割" : "ge", + "歌" : "ge", + "革" : "ge", + "阁" : "ge", + "格" : "ge", + "隔" : "ge", + "嗝" : "ge", + "膈" : "ge", + "骼" : "ge", + "镉" : "ge", + "舸" : "ge", + "葛" : "ge", + "个" : "ge", + "各" : "ge", + "虼" : "ge", + "硌" : "ge", + "铬" : "ge", + "根" : "gen", + "跟" : "gen", + "哏" : "gen", + "亘" : "gen", + "艮" : "gen", + "茛" : "gen", + "庚" : "geng", + "耕" : "geng", + "浭" : "geng", + "赓" : "geng", + "羹" : "geng", + "埂" : "geng", + "耿" : "geng", + "哽" : "geng", + "绠" : "geng", + "梗" : "geng", + "鲠" : "geng", + "更" : "geng", + "工" : "gong", + "弓" : "gong", + "公" : "gong", + "功" : "gong", + "攻" : "gong", + "肱" : "gong", + "宫" : "gong", + "恭" : "gong", + "蚣" : "gong", + "躬" : "gong", + "龚" : "gong", + "塨" : "gong", + "觥" : "gong", + "巩" : "gong", + "汞" : "gong", + "拱" : "gong", + "珙" : "gong", + "共" : "gong", + "贡" : "gong", + "供" : "gong", + "勾" : "gou", + "佝" : "gou", + "沟" : "gou", + "钩" : "gou", + "篝" : "gou", + "苟" : "gou", + "岣" : "gou", + "狗" : "gou", + "枸" : "gou", + "构" : "gou", + "购" : "gou", + "诟" : "gou", + "垢" : "gou", + "够" : "gou", + "彀" : "gou", + "媾" : "gou", + "觏" : "gou", + "估" : "gu", + "咕" : "gu", + "沽" : "gu", + "孤" : "gu", + "姑" : "gu", + "轱" : "gu", + "鸪" : "gu", + "菰" : "gu", + "菇" : "gu", + "蛄" : "gu", + "蓇" : "gu", + "辜" : "gu", + "酤" : "gu", + "觚" : "gu", + "毂" : "gu", + "箍" : "gu", + "古" : "gu", + "谷" : "gu", + "汩" : "gu", + "诂" : "gu", + "股" : "gu", + "骨" : "gu", + "牯" : "gu", + "钴" : "gu", + "羖" : "gu", + "蛊" : "gu", + "鼓" : "gu", + "榾" : "gu", + "鹘" : "gu", + "臌" : "gu", + "瀔" : "gu", + "固" : "gu", + "故" : "gu", + "顾" : "gu", + "梏" : "gu", + "崮" : "gu", + "雇" : "gu", + "锢" : "gu", + "痼" : "gu", + "瓜" : "gua", + "刮" : "gua", + "胍" : "gua", + "鸹" : "gua", + "剐" : "gua", + "寡" : "gua", + "卦" : "gua", + "诖" : "gua", + "挂" : "gua", + "褂" : "gua", + "乖" : "guai", + "拐" : "guai", + "怪" : "guai", + "关" : "guan", + "观" : "guan", + "官" : "guan", + "倌" : "guan", + "蒄" : "guan", + "棺" : "guan", + "瘝" : "guan", + "鳏" : "guan", + "馆" : "guan", + "管" : "guan", + "贯" : "guan", + "冠" : "guan", + "掼" : "guan", + "惯" : "guan", + "祼" : "guan", + "盥" : "guan", + "灌" : "guan", + "瓘" : "guan", + "鹳" : "guan", + "罐" : "guan", + "琯" : "guan", + "光" : "guang", + "咣" : "guang", + "胱" : "guang", + "广" : "guang", + "犷" : "guang", + "桄" : "guang", + "逛" : "guang", + "归" : "gui", + "圭" : "gui", + "龟" : "gui", + "妫" : "gui", + "规" : "gui", + "皈" : "gui", + "闺" : "gui", + "硅" : "gui", + "瑰" : "gui", + "鲑" : "gui", + "宄" : "gui", + "轨" : "gui", + "庋" : "gui", + "匦" : "gui", + "诡" : "gui", + "鬼" : "gui", + "姽" : "gui", + "癸" : "gui", + "晷" : "gui", + "簋" : "gui", + "柜" : "gui", + "炅" : "gui", + "刿" : "gui", + "刽" : "gui", + "贵" : "gui", + "桂" : "gui", + "跪" : "gui", + "鳜" : "gui", + "衮" : "gun", + "绲" : "gun", + "辊" : "gun", + "滚" : "gun", + "磙" : "gun", + "鲧" : "gun", + "棍" : "gun", + "埚" : "guo", + "郭" : "guo", + "啯" : "guo", + "崞" : "guo", + "聒" : "guo", + "锅" : "guo", + "蝈" : "guo", + "国" : "guo", + "帼" : "guo", + "虢" : "guo", + "果" : "guo", + "椁" : "guo", + "蜾" : "guo", + "裹" : "guo", + "过" : "guo", + "哈" : "ha", + "铪" : "ha", + "孩" : "hai", + "骸" : "hai", + "胲" : "hai", + "海" : "hai", + "醢" : "hai", + "亥" : "hai", + "骇" : "hai", + "害" : "hai", + "嗐" : "hai", + "嗨" : "hai", + "顸" : "han", + "蚶" : "han", + "酣" : "han", + "憨" : "han", + "鼾" : "han", + "邗" : "han", + "邯" : "han", + "含" : "han", + "函" : "han", + "晗" : "han", + "焓" : "han", + "涵" : "han", + "韩" : "han", + "寒" : "han", + "罕" : "han", + "喊" : "han", + "蔊" : "han", + "汉" : "han", + "汗" : "han", + "旱" : "han", + "捍" : "han", + "悍" : "han", + "菡" : "han", + "焊" : "han", + "撖" : "han", + "撼" : "han", + "翰" : "han", + "憾" : "han", + "瀚" : "han", + "夯" : "hang", + "杭" : "hang", + "绗" : "hang", + "航" : "hang", + "沆" : "hang", + "蒿" : "hao", + "薅" : "hao", + "嚆" : "hao", + "蚝" : "hao", + "毫" : "hao", + "嗥" : "hao", + "豪" : "hao", + "壕" : "hao", + "嚎" : "hao", + "濠" : "hao", + "好" : "hao", + "郝" : "hao", + "号" : "hao", + "昊" : "hao", + "耗" : "hao", + "浩" : "hao", + "皓" : "hao", + "滈" : "hao", + "颢" : "hao", + "灏" : "hao", + "诃" : "he", + "呵" : "he", + "喝" : "he", + "嗬" : "he", + "禾" : "he", + "合" : "he", + "何" : "he", + "劾" : "he", + "河" : "he", + "曷" : "he", + "阂" : "he", + "盍" : "he", + "荷" : "he", + "菏" : "he", + "盒" : "he", + "涸" : "he", + "颌" : "he", + "阖" : "he", + "贺" : "he", + "赫" : "he", + "褐" : "he", + "鹤" : "he", + "壑" : "he", + "黑" : "hei", + "嘿" : "hei", + "痕" : "hen", + "很" : "hen", + "狠" : "hen", + "恨" : "hen", + "亨" : "heng", + "恒" : "heng", + "珩" : "heng", + "横" : "heng", + "衡" : "heng", + "蘅" : "heng", + "啈" : "heng", + "轰" : "hong", + "訇" : "hong", + "烘" : "hong", + "薨" : "hong", + "弘" : "hong", + "红" : "hong", + "闳" : "hong", + "宏" : "hong", + "荭" : "hong", + "虹" : "hong", + "竑" : "hong", + "洪" : "hong", + "鸿" : "hong", + "哄" : "hong", + "讧" : "hong", + "吽" : "hong", + "齁" : "hou", + "侯" : "hou", + "喉" : "hou", + "猴" : "hou", + "瘊" : "hou", + "骺" : "hou", + "篌" : "hou", + "糇" : "hou", + "吼" : "hou", + "后" : "hou", + "郈" : "hou", + "厚" : "hou", + "垕" : "hou", + "逅" : "hou", + "候" : "hou", + "堠" : "hou", + "鲎" : "hou", + "乎" : "hu", + "呼" : "hu", + "忽" : "hu", + "轷" : "hu", + "烀" : "hu", + "惚" : "hu", + "滹" : "hu", + "囫" : "hu", + "狐" : "hu", + "弧" : "hu", + "胡" : "hu", + "壶" : "hu", + "斛" : "hu", + "葫" : "hu", + "猢" : "hu", + "湖" : "hu", + "瑚" : "hu", + "鹕" : "hu", + "槲" : "hu", + "蝴" : "hu", + "糊" : "hu", + "醐" : "hu", + "觳" : "hu", + "虎" : "hu", + "唬" : "hu", + "琥" : "hu", + "互" : "hu", + "户" : "hu", + "冱" : "hu", + "护" : "hu", + "沪" : "hu", + "枑" : "hu", + "怙" : "hu", + "戽" : "hu", + "笏" : "hu", + "瓠" : "hu", + "扈" : "hu", + "鹱" : "hu", + "花" : "hua", + "砉" : "hua", + "华" : "hua", + "哗" : "hua", + "骅" : "hua", + "铧" : "hua", + "猾" : "hua", + "滑" : "hua", + "化" : "hua", + "画" : "hua", + "话" : "hua", + "桦" : "hua", + "婳" : "hua", + "觟" : "hua", + "怀" : "huai", + "徊" : "huai", + "淮" : "huai", + "槐" : "huai", + "踝" : "huai", + "耲" : "huai", + "坏" : "huai", + "欢" : "huan", + "獾" : "huan", + "环" : "huan", + "洹" : "huan", + "桓" : "huan", + "萑" : "huan", + "寰" : "huan", + "缳" : "huan", + "缓" : "huan", + "幻" : "huan", + "奂" : "huan", + "宦" : "huan", + "换" : "huan", + "唤" : "huan", + "涣" : "huan", + "浣" : "huan", + "患" : "huan", + "焕" : "huan", + "痪" : "huan", + "豢" : "huan", + "漶" : "huan", + "鲩" : "huan", + "擐" : "huan", + "肓" : "huang", + "荒" : "huang", + "塃" : "huang", + "慌" : "huang", + "皇" : "huang", + "黄" : "huang", + "凰" : "huang", + "隍" : "huang", + "喤" : "huang", + "遑" : "huang", + "徨" : "huang", + "湟" : "huang", + "惶" : "huang", + "媓" : "huang", + "煌" : "huang", + "锽" : "huang", + "潢" : "huang", + "璜" : "huang", + "蝗" : "huang", + "篁" : "huang", + "艎" : "huang", + "磺" : "huang", + "癀" : "huang", + "蟥" : "huang", + "簧" : "huang", + "鳇" : "huang", + "恍" : "huang", + "晃" : "huang", + "谎" : "huang", + "幌" : "huang", + "滉" : "huang", + "皝" : "huang", + "灰" : "hui", + "诙" : "hui", + "挥" : "hui", + "恢" : "hui", + "晖" : "hui", + "辉" : "hui", + "麾" : "hui", + "徽" : "hui", + "隳" : "hui", + "回" : "hui", + "茴" : "hui", + "洄" : "hui", + "蛔" : "hui", + "悔" : "hui", + "毁" : "hui", + "卉" : "hui", + "汇" : "hui", + "讳" : "hui", + "荟" : "hui", + "浍" : "hui", + "诲" : "hui", + "绘" : "hui", + "恚" : "hui", + "贿" : "hui", + "烩" : "hui", + "彗" : "hui", + "晦" : "hui", + "秽" : "hui", + "惠" : "hui", + "喙" : "hui", + "慧" : "hui", + "蕙" : "hui", + "蟪" : "hui", + "珲" : "hun", + "昏" : "hun", + "荤" : "hun", + "阍" : "hun", + "惛" : "hun", + "婚" : "hun", + "浑" : "hun", + "馄" : "hun", + "混" : "hun", + "魂" : "hun", + "诨" : "hun", + "溷" : "hun", + "耠" : "huo", + "劐" : "huo", + "豁" : "huo", + "活" : "huo", + "火" : "huo", + "伙" : "huo", + "钬" : "huo", + "夥" : "huo", + "或" : "huo", + "货" : "huo", + "获" : "huo", + "祸" : "huo", + "惑" : "huo", + "霍" : "huo", + "镬" : "huo", + "攉" : "huo", + "藿" : "huo", + "嚯" : "huo", + "讥" : "ji", + "击" : "ji", + "叽" : "ji", + "饥" : "ji", + "玑" : "ji", + "圾" : "ji", + "芨" : "ji", + "机" : "ji", + "乩" : "ji", + "肌" : "ji", + "矶" : "ji", + "鸡" : "ji", + "剞" : "ji", + "唧" : "ji", + "积" : "ji", + "笄" : "ji", + "屐" : "ji", + "姬" : "ji", + "基" : "ji", + "犄" : "ji", + "嵇" : "ji", + "畸" : "ji", + "跻" : "ji", + "箕" : "ji", + "齑" : "ji", + "畿" : "ji", + "墼" : "ji", + "激" : "ji", + "羁" : "ji", + "及" : "ji", + "吉" : "ji", + "岌" : "ji", + "汲" : "ji", + "级" : "ji", + "极" : "ji", + "即" : "ji", + "佶" : "ji", + "笈" : "ji", + "急" : "ji", + "疾" : "ji", + "棘" : "ji", + "集" : "ji", + "蒺" : "ji", + "楫" : "ji", + "辑" : "ji", + "嫉" : "ji", + "瘠" : "ji", + "藉" : "ji", + "籍" : "ji", + "几" : "ji", + "己" : "ji", + "虮" : "ji", + "挤" : "ji", + "脊" : "ji", + "掎" : "ji", + "戟" : "ji", + "麂" : "ji", + "计" : "ji", + "记" : "ji", + "伎" : "ji", + "纪" : "ji", + "技" : "ji", + "忌" : "ji", + "际" : "ji", + "妓" : "ji", + "季" : "ji", + "剂" : "ji", + "迹" : "ji", + "济" : "ji", + "既" : "ji", + "觊" : "ji", + "继" : "ji", + "偈" : "ji", + "祭" : "ji", + "悸" : "ji", + "寄" : "ji", + "寂" : "ji", + "绩" : "ji", + "暨" : "ji", + "稷" : "ji", + "鲫" : "ji", + "髻" : "ji", + "冀" : "ji", + "骥" : "ji", + "加" : "jia", + "佳" : "jia", + "枷" : "jia", + "浃" : "jia", + "痂" : "jia", + "家" : "jia", + "袈" : "jia", + "嘉" : "jia", + "镓" : "jia", + "荚" : "jia", + "戛" : "jia", + "颊" : "jia", + "甲" : "jia", + "胛" : "jia", + "钾" : "jia", + "假" : "jia", + "价" : "jia", + "驾" : "jia", + "架" : "jia", + "嫁" : "jia", + "稼" : "jia", + "戋" : "jian", + "尖" : "jian", + "奸" : "jian", + "歼" : "jian", + "坚" : "jian", + "间" : "jian", + "肩" : "jian", + "艰" : "jian", + "监" : "jian", + "兼" : "jian", + "菅" : "jian", + "笺" : "jian", + "缄" : "jian", + "煎" : "jian", + "拣" : "jian", + "茧" : "jian", + "柬" : "jian", + "俭" : "jian", + "捡" : "jian", + "检" : "jian", + "减" : "jian", + "剪" : "jian", + "睑" : "jian", + "简" : "jian", + "碱" : "jian", + "见" : "jian", + "件" : "jian", + "饯" : "jian", + "建" : "jian", + "荐" : "jian", + "贱" : "jian", + "剑" : "jian", + "健" : "jian", + "舰" : "jian", + "涧" : "jian", + "渐" : "jian", + "谏" : "jian", + "践" : "jian", + "锏" : "jian", + "毽" : "jian", + "腱" : "jian", + "溅" : "jian", + "鉴" : "jian", + "键" : "jian", + "僭" : "jian", + "箭" : "jian", + "江" : "jiang", + "将" : "jiang", + "姜" : "jiang", + "豇" : "jiang", + "浆" : "jiang", + "僵" : "jiang", + "缰" : "jiang", + "疆" : "jiang", + "讲" : "jiang", + "奖" : "jiang", + "桨" : "jiang", + "蒋" : "jiang", + "匠" : "jiang", + "酱" : "jiang", + "犟" : "jiang", + "糨" : "jiang", + "交" : "jiao", + "郊" : "jiao", + "浇" : "jiao", + "娇" : "jiao", + "姣" : "jiao", + "骄" : "jiao", + "胶" : "jiao", + "椒" : "jiao", + "蛟" : "jiao", + "焦" : "jiao", + "跤" : "jiao", + "蕉" : "jiao", + "礁" : "jiao", + "佼" : "jiao", + "狡" : "jiao", + "饺" : "jiao", + "绞" : "jiao", + "铰" : "jiao", + "矫" : "jiao", + "皎" : "jiao", + "脚" : "jiao", + "搅" : "jiao", + "剿" : "jiao", + "缴" : "jiao", + "叫" : "jiao", + "轿" : "jiao", + "较" : "jiao", + "教" : "jiao", + "窖" : "jiao", + "酵" : "jiao", + "侥" : "jiao", + "阶" : "jie", + "皆" : "jie", + "接" : "jie", + "秸" : "jie", + "揭" : "jie", + "嗟" : "jie", + "街" : "jie", + "孑" : "jie", + "节" : "jie", + "讦" : "jie", + "劫" : "jie", + "杰" : "jie", + "诘" : "jie", + "洁" : "jie", + "结" : "jie", + "捷" : "jie", + "睫" : "jie", + "截" : "jie", + "碣" : "jie", + "竭" : "jie", + "姐" : "jie", + "解" : "jie", + "介" : "jie", + "戒" : "jie", + "届" : "jie", + "界" : "jie", + "疥" : "jie", + "诫" : "jie", + "借" : "jie", + "巾" : "jin", + "斤" : "jin", + "今" : "jin", + "金" : "jin", + "津" : "jin", + "矜" : "jin", + "筋" : "jin", + "襟" : "jin", + "仅" : "jin", + "紧" : "jin", + "锦" : "jin", + "谨" : "jin", + "尽" : "jin", + "进" : "jin", + "近" : "jin", + "晋" : "jin", + "烬" : "jin", + "浸" : "jin", + "禁" : "jin", + "觐" : "jin", + "噤" : "jin", + "茎" : "jing", + "京" : "jing", + "泾" : "jing", + "经" : "jing", + "菁" : "jing", + "惊" : "jing", + "晶" : "jing", + "睛" : "jing", + "粳" : "jing", + "兢" : "jing", + "精" : "jing", + "鲸" : "jing", + "井" : "jing", + "阱" : "jing", + "刭" : "jing", + "景" : "jing", + "儆" : "jing", + "警" : "jing", + "径" : "jing", + "净" : "jing", + "痉" : "jing", + "竞" : "jing", + "竟" : "jing", + "敬" : "jing", + "靖" : "jing", + "静" : "jing", + "境" : "jing", + "镜" : "jing", + "迥" : "jiong", + "炯" : "jiong", + "窘" : "jiong", + "纠" : "jiu", + "鸠" : "jiu", + "究" : "jiu", + "赳" : "jiu", + "阄" : "jiu", + "揪" : "jiu", + "啾" : "jiu", + "九" : "jiu", + "久" : "jiu", + "玖" : "jiu", + "灸" : "jiu", + "韭" : "jiu", + "酒" : "jiu", + "旧" : "jiu", + "臼" : "jiu", + "咎" : "jiu", + "柩" : "jiu", + "救" : "jiu", + "厩" : "jiu", + "就" : "jiu", + "舅" : "jiu", + "鹫" : "jiu", + "军" : "jun", + "均" : "jun", + "君" : "jun", + "钧" : "jun", + "菌" : "jun", + "皲" : "jun", + "俊" : "jun", + "郡" : "jun", + "峻" : "jun", + "骏" : "jun", + "竣" : "jun", + "拘" : "ju", + "狙" : "ju", + "居" : "ju", + "驹" : "ju", + "掬" : "ju", + "雎" : "ju", + "鞠" : "ju", + "局" : "ju", + "菊" : "ju", + "焗" : "ju", + "橘" : "ju", + "咀" : "ju", + "沮" : "ju", + "矩" : "ju", + "举" : "ju", + "龃" : "ju", + "巨" : "ju", + "拒" : "ju", + "具" : "ju", + "炬" : "ju", + "俱" : "ju", + "剧" : "ju", + "据" : "ju", + "距" : "ju", + "惧" : "ju", + "飓" : "ju", + "锯" : "ju", + "聚" : "ju", + "踞" : "ju", + "捐" : "juan", + "涓" : "juan", + "娟" : "juan", + "鹃" : "juan", + "卷" : "juan", + "倦" : "juan", + "绢" : "juan", + "眷" : "juan", + "隽" : "juan", + "撅" : "jue", + "噘" : "jue", + "决" : "jue", + "诀" : "jue", + "抉" : "jue", + "绝" : "jue", + "掘" : "jue", + "崛" : "jue", + "厥" : "jue", + "谲" : "jue", + "蕨" : "jue", + "爵" : "jue", + "蹶" : "jue", + "矍" : "jue", + "倔" : "jue", + "咔" : "ka", + "开" : "kai", + "揩" : "kai", + "凯" : "kai", + "铠" : "kai", + "慨" : "kai", + "楷" : "kai", + "忾" : "kai", + "刊" : "kan", + "勘" : "kan", + "龛" : "kan", + "堪" : "kan", + "坎" : "kan", + "侃" : "kan", + "砍" : "kan", + "槛" : "kan", + "看" : "kan", + "瞰" : "kan", + "康" : "kang", + "慷" : "kang", + "糠" : "kang", + "亢" : "kang", + "伉" : "kang", + "抗" : "kang", + "炕" : "kang", + "考" : "kao", + "拷" : "kao", + "烤" : "kao", + "铐" : "kao", + "犒" : "kao", + "靠" : "kao", + "苛" : "ke", + "轲" : "ke", + "科" : "ke", + "棵" : "ke", + "搕" : "ke", + "嗑" : "ke", + "稞" : "ke", + "窠" : "ke", + "颗" : "ke", + "磕" : "ke", + "瞌" : "ke", + "蝌" : "ke", + "可" : "ke", + "坷" : "ke", + "渴" : "ke", + "克" : "ke", + "刻" : "ke", + "恪" : "ke", + "客" : "ke", + "课" : "ke", + "肯" : "ken", + "垦" : "ken", + "恳" : "ken", + "啃" : "ken", + "坑" : "keng", + "铿" : "keng", + "空" : "kong", + "孔" : "kong", + "恐" : "kong", + "控" : "kong", + "抠" : "kou", + "口" : "kou", + "叩" : "kou", + "扣" : "kou", + "寇" : "kou", + "蔻" : "kou", + "枯" : "ku", + "哭" : "ku", + "窟" : "ku", + "骷" : "ku", + "苦" : "ku", + "库" : "ku", + "绔" : "ku", + "裤" : "ku", + "酷" : "ku", + "夸" : "kua", + "垮" : "kua", + "挎" : "kua", + "胯" : "kua", + "跨" : "kua", + "块" : "kuai", + "快" : "kuai", + "侩" : "kuai", + "脍" : "kuai", + "筷" : "kuai", + "宽" : "kuan", + "髋" : "kuan", + "款" : "kuan", + "诓" : "kuang", + "哐" : "kuang", + "筐" : "kuang", + "狂" : "kuang", + "诳" : "kuang", + "旷" : "kuang", + "况" : "kuang", + "矿" : "kuang", + "框" : "kuang", + "眶" : "kuang", + "亏" : "kui", + "盔" : "kui", + "窥" : "kui", + "葵" : "kui", + "魁" : "kui", + "傀" : "kui", + "匮" : "kui", + "馈" : "kui", + "愧" : "kui", + "坤" : "kun", + "昆" : "kun", + "鲲" : "kun", + "捆" : "kun", + "困" : "kun", + "扩" : "kuo", + "括" : "kuo", + "阔" : "kuo", + "廓" : "kuo", + "垃" : "la", + "拉" : "la", + "啦" : "la", + "邋" : "la", + "旯" : "la", + "喇" : "la", + "腊" : "la", + "蜡" : "la", + "辣" : "la", + "来" : "lai", + "莱" : "lai", + "徕" : "lai", + "睐" : "lai", + "赖" : "lai", + "癞" : "lai", + "籁" : "lai", + "兰" : "lan", + "岚" : "lan", + "拦" : "lan", + "栏" : "lan", + "婪" : "lan", + "阑" : "lan", + "蓝" : "lan", + "澜" : "lan", + "褴" : "lan", + "篮" : "lan", + "览" : "lan", + "揽" : "lan", + "缆" : "lan", + "榄" : "lan", + "懒" : "lan", + "烂" : "lan", + "滥" : "lan", + "啷" : "lang", + "郎" : "lang", + "狼" : "lang", + "琅" : "lang", + "廊" : "lang", + "榔" : "lang", + "锒" : "lang", + "螂" : "lang", + "朗" : "lang", + "浪" : "lang", + "捞" : "lao", + "劳" : "lao", + "牢" : "lao", + "崂" : "lao", + "老" : "lao", + "佬" : "lao", + "姥" : "lao", + "唠" : "lao", + "烙" : "lao", + "涝" : "lao", + "酪" : "lao", + "雷" : "lei", + "羸" : "lei", + "垒" : "lei", + "磊" : "lei", + "蕾" : "lei", + "儡" : "lei", + "肋" : "lei", + "泪" : "lei", + "类" : "lei", + "累" : "lei", + "擂" : "lei", + "嘞" : "lei", + "棱" : "leng", + "楞" : "leng", + "冷" : "leng", + "睖" : "leng", + "厘" : "li", + "狸" : "li", + "离" : "li", + "梨" : "li", + "犁" : "li", + "鹂" : "li", + "喱" : "li", + "蜊" : "li", + "漓" : "li", + "璃" : "li", + "黎" : "li", + "罹" : "li", + "篱" : "li", + "蠡" : "li", + "礼" : "li", + "李" : "li", + "里" : "li", + "俚" : "li", + "逦" : "li", + "哩" : "li", + "娌" : "li", + "理" : "li", + "鲤" : "li", + "力" : "li", + "历" : "li", + "厉" : "li", + "立" : "li", + "吏" : "li", + "丽" : "li", + "励" : "li", + "呖" : "li", + "利" : "li", + "沥" : "li", + "枥" : "li", + "例" : "li", + "戾" : "li", + "隶" : "li", + "荔" : "li", + "俐" : "li", + "莉" : "li", + "莅" : "li", + "栗" : "li", + "砾" : "li", + "蛎" : "li", + "唳" : "li", + "笠" : "li", + "粒" : "li", + "雳" : "li", + "痢" : "li", + "连" : "lian", + "怜" : "lian", + "帘" : "lian", + "莲" : "lian", + "涟" : "lian", + "联" : "lian", + "廉" : "lian", + "鲢" : "lian", + "镰" : "lian", + "敛" : "lian", + "脸" : "lian", + "练" : "lian", + "炼" : "lian", + "恋" : "lian", + "殓" : "lian", + "链" : "lian", + "良" : "liang", + "凉" : "liang", + "梁" : "liang", + "粮" : "liang", + "粱" : "liang", + "两" : "liang", + "魉" : "liang", + "亮" : "liang", + "谅" : "liang", + "辆" : "liang", + "靓" : "liang", + "量" : "liang", + "晾" : "liang", + "踉" : "liang", + "辽" : "liao", + "疗" : "liao", + "聊" : "liao", + "僚" : "liao", + "寥" : "liao", + "撩" : "liao", + "嘹" : "liao", + "獠" : "liao", + "潦" : "liao", + "缭" : "liao", + "燎" : "liao", + "料" : "liao", + "撂" : "liao", + "瞭" : "liao", + "镣" : "liao", + "咧" : "lie", + "列" : "lie", + "劣" : "lie", + "冽" : "lie", + "烈" : "lie", + "猎" : "lie", + "裂" : "lie", + "趔" : "lie", + "拎" : "lin", + "邻" : "lin", + "林" : "lin", + "临" : "lin", + "淋" : "lin", + "琳" : "lin", + "粼" : "lin", + "嶙" : "lin", + "潾" : "lin", + "霖" : "lin", + "磷" : "lin", + "鳞" : "lin", + "麟" : "lin", + "凛" : "lin", + "檩" : "lin", + "吝" : "lin", + "赁" : "lin", + "躏" : "lin", + "伶" : "ling", + "灵" : "ling", + "苓" : "ling", + "囹" : "ling", + "泠" : "ling", + "玲" : "ling", + "瓴" : "ling", + "铃" : "ling", + "凌" : "ling", + "陵" : "ling", + "聆" : "ling", + "菱" : "ling", + "棂" : "ling", + "蛉" : "ling", + "翎" : "ling", + "羚" : "ling", + "绫" : "ling", + "零" : "ling", + "龄" : "ling", + "岭" : "ling", + "领" : "ling", + "另" : "ling", + "令" : "ling", + "溜" : "liu", + "熘" : "liu", + "刘" : "liu", + "浏" : "liu", + "留" : "liu", + "流" : "liu", + "琉" : "liu", + "硫" : "liu", + "馏" : "liu", + "榴" : "liu", + "瘤" : "liu", + "柳" : "liu", + "绺" : "liu", + "六" : "liu", + "遛" : "liu", + "龙" : "long", + "咙" : "long", + "珑" : "long", + "胧" : "long", + "聋" : "long", + "笼" : "long", + "隆" : "long", + "窿" : "long", + "陇" : "long", + "拢" : "long", + "垄" : "long", + "娄" : "lou", + "楼" : "lou", + "髅" : "lou", + "搂" : "lou", + "篓" : "lou", + "陋" : "lou", + "镂" : "lou", + "漏" : "lou", + "喽" : "lou", + "撸" : "lu", + "卢" : "lu", + "芦" : "lu", + "庐" : "lu", + "炉" : "lu", + "泸" : "lu", + "鸬" : "lu", + "颅" : "lu", + "鲈" : "lu", + "卤" : "lu", + "虏" : "lu", + "掳" : "lu", + "鲁" : "lu", + "橹" : "lu", + "录" : "lu", + "赂" : "lu", + "鹿" : "lu", + "禄" : "lu", + "路" : "lu", + "箓" : "lu", + "漉" : "lu", + "戮" : "lu", + "鹭" : "lu", + "麓" : "lu", + "峦" : "luan", + "孪" : "luan", + "挛" : "luan", + "鸾" : "luan", + "卵" : "luan", + "乱" : "luan", + "抡" : "lun", + "仑" : "lun", + "伦" : "lun", + "囵" : "lun", + "沦" : "lun", + "轮" : "lun", + "论" : "lun", + "啰" : "luo", + "罗" : "luo", + "萝" : "luo", + "逻" : "luo", + "锣" : "luo", + "箩" : "luo", + "骡" : "luo", + "螺" : "luo", + "裸" : "luo", + "洛" : "luo", + "络" : "luo", + "骆" : "luo", + "摞" : "luo", + "漯" : "luo", + "驴" : "lv", + "榈" : "lv", + "吕" : "lv", + "侣" : "lv", + "旅" : "lv", + "铝" : "lv", + "屡" : "lv", + "缕" : "lv", + "膂" : "lv", + "褛" : "lv", + "履" : "lv", + "律" : "lv", + "虑" : "lv", + "氯" : "lv", + "滤" : "lv", + "掠" : "lve", + "略" : "lve", + "妈" : "ma", + "麻" : "ma", + "蟆" : "ma", + "马" : "ma", + "犸" : "ma", + "玛" : "ma", + "码" : "ma", + "蚂" : "ma", + "骂" : "ma", + "吗" : "ma", + "嘛" : "ma", + "霾" : "mai", + "买" : "mai", + "迈" : "mai", + "麦" : "mai", + "卖" : "mai", + "霡" : "mai", + "蛮" : "man", + "馒" : "man", + "瞒" : "man", + "满" : "man", + "曼" : "man", + "谩" : "man", + "幔" : "man", + "漫" : "man", + "慢" : "man", + "牤" : "mang", + "芒" : "mang", + "忙" : "mang", + "盲" : "mang", + "氓" : "mang", + "茫" : "mang", + "莽" : "mang", + "漭" : "mang", + "蟒" : "mang", + "猫" : "mao", + "毛" : "mao", + "矛" : "mao", + "茅" : "mao", + "牦" : "mao", + "锚" : "mao", + "髦" : "mao", + "蝥" : "mao", + "蟊" : "mao", + "冇" : "mao", + "卯" : "mao", + "铆" : "mao", + "茂" : "mao", + "冒" : "mao", + "贸" : "mao", + "袤" : "mao", + "帽" : "mao", + "貌" : "mao", + "玫" : "mei", + "枚" : "mei", + "眉" : "mei", + "莓" : "mei", + "梅" : "mei", + "媒" : "mei", + "楣" : "mei", + "煤" : "mei", + "酶" : "mei", + "霉" : "mei", + "每" : "mei", + "美" : "mei", + "镁" : "mei", + "妹" : "mei", + "昧" : "mei", + "袂" : "mei", + "寐" : "mei", + "媚" : "mei", + "魅" : "mei", + "门" : "men", + "扪" : "men", + "闷" : "men", + "焖" : "men", + "懑" : "men", + "们" : "men", + "虻" : "meng", + "萌" : "meng", + "蒙" : "meng", + "盟" : "meng", + "檬" : "meng", + "曚" : "meng", + "朦" : "meng", + "猛" : "meng", + "锰" : "meng", + "蜢" : "meng", + "懵" : "meng", + "孟" : "meng", + "梦" : "meng", + "咪" : "mi", + "眯" : "mi", + "弥" : "mi", + "迷" : "mi", + "猕" : "mi", + "谜" : "mi", + "醚" : "mi", + "糜" : "mi", + "麋" : "mi", + "靡" : "mi", + "米" : "mi", + "弭" : "mi", + "觅" : "mi", + "密" : "mi", + "幂" : "mi", + "谧" : "mi", + "蜜" : "mi", + "眠" : "mian", + "绵" : "mian", + "棉" : "mian", + "免" : "mian", + "勉" : "mian", + "娩" : "mian", + "冕" : "mian", + "渑" : "mian", + "湎" : "mian", + "缅" : "mian", + "腼" : "mian", + "面" : "mian", + "喵" : "miao", + "苗" : "miao", + "描" : "miao", + "瞄" : "miao", + "秒" : "miao", + "渺" : "miao", + "藐" : "miao", + "妙" : "miao", + "庙" : "miao", + "缥" : "miao", + "咩" : "mie", + "灭" : "mie", + "蔑" : "mie", + "篾" : "mie", + "乜" : "mie", + "民" : "min", + "皿" : "min", + "抿" : "min", + "泯" : "min", + "闽" : "min", + "悯" : "min", + "敏" : "min", + "名" : "ming", + "明" : "ming", + "鸣" : "ming", + "茗" : "ming", + "冥" : "ming", + "铭" : "ming", + "瞑" : "ming", + "螟" : "ming", + "酩" : "ming", + "命" : "ming", + "谬" : "miu", + "摸" : "mo", + "馍" : "mo", + "摹" : "mo", + "膜" : "mo", + "摩" : "mo", + "磨" : "mo", + "蘑" : "mo", + "魔" : "mo", + "末" : "mo", + "茉" : "mo", + "殁" : "mo", + "沫" : "mo", + "陌" : "mo", + "莫" : "mo", + "秣" : "mo", + "蓦" : "mo", + "漠" : "mo", + "寞" : "mo", + "墨" : "mo", + "默" : "mo", + "嬷" : "mo", + "缪" : "mou", + "哞" : "mou", + "眸" : "mou", + "谋" : "mou", + "某" : "mou", + "母" : "mu", + "牡" : "mu", + "亩" : "mu", + "拇" : "mu", + "姆" : "mu", + "木" : "mu", + "目" : "mu", + "沐" : "mu", + "苜" : "mu", + "牧" : "mu", + "钼" : "mu", + "募" : "mu", + "墓" : "mu", + "幕" : "mu", + "睦" : "mu", + "慕" : "mu", + "暮" : "mu", + "穆" : "mu", + "拿" : "na", + "呐" : "na", + "纳" : "na", + "钠" : "na", + "衲" : "na", + "捺" : "na", + "乃" : "nai", + "奶" : "nai", + "氖" : "nai", + "奈" : "nai", + "耐" : "nai", + "囡" : "nan", + "男" : "nan", + "南" : "nan", + "难" : "nan", + "喃" : "nan", + "楠" : "nan", + "赧" : "nan", + "腩" : "nan", + "囔" : "nang", + "囊" : "nang", + "孬" : "nao", + "呶" : "nao", + "挠" : "nao", + "恼" : "nao", + "脑" : "nao", + "瑙" : "nao", + "闹" : "nao", + "淖" : "nao", + "讷" : "ne", + "馁" : "nei", + "内" : "nei", + "嫩" : "nen", + "恁" : "nen", + "能" : "neng", + "嗯" : "ng", + "妮" : "ni", + "尼" : "ni", + "泥" : "ni", + "怩" : "ni", + "倪" : "ni", + "霓" : "ni", + "拟" : "ni", + "你" : "ni", + "旎" : "ni", + "昵" : "ni", + "逆" : "ni", + "匿" : "ni", + "腻" : "ni", + "溺" : "ni", + "拈" : "nian", + "蔫" : "nian", + "年" : "nian", + "黏" : "nian", + "捻" : "nian", + "辇" : "nian", + "撵" : "nian", + "碾" : "nian", + "廿" : "nian", + "念" : "nian", + "娘" : "niang", + "酿" : "niang", + "鸟" : "niao", + "袅" : "niao", + "尿" : "niao", + "捏" : "nie", + "聂" : "nie", + "涅" : "nie", + "嗫" : "nie", + "镊" : "nie", + "镍" : "nie", + "蹑" : "nie", + "孽" : "nie", + "您" : "nin", + "宁" : "ning", + "咛" : "ning", + "狞" : "ning", + "柠" : "ning", + "凝" : "ning", + "拧" : "ning", + "佞" : "ning", + "泞" : "ning", + "妞" : "niu", + "牛" : "niu", + "扭" : "niu", + "忸" : "niu", + "纽" : "niu", + "钮" : "niu", + "农" : "nong", + "哝" : "nong", + "浓" : "nong", + "脓" : "nong", + "弄" : "nong", + "奴" : "nu", + "驽" : "nu", + "努" : "nu", + "弩" : "nu", + "怒" : "nu", + "暖" : "nuan", + "疟" : "nue", + "虐" : "nue", + "挪" : "nuo", + "诺" : "nuo", + "喏" : "nuo", + "懦" : "nuo", + "糯" : "nuo", + "女" : "nv", + "噢" : "o", + "讴" : "ou", + "瓯" : "ou", + "欧" : "ou", + "殴" : "ou", + "鸥" : "ou", + "呕" : "ou", + "偶" : "ou", + "藕" : "ou", + "怄" : "ou", + "趴" : "pa", + "啪" : "pa", + "葩" : "pa", + "杷" : "pa", + "爬" : "pa", + "琶" : "pa", + "帕" : "pa", + "怕" : "pa", + "拍" : "pai", + "排" : "pai", + "徘" : "pai", + "牌" : "pai", + "哌" : "pai", + "派" : "pai", + "湃" : "pai", + "潘" : "pan", + "攀" : "pan", + "爿" : "pan", + "盘" : "pan", + "磐" : "pan", + "蹒" : "pan", + "蟠" : "pan", + "判" : "pan", + "盼" : "pan", + "叛" : "pan", + "畔" : "pan", + "乓" : "pang", + "滂" : "pang", + "庞" : "pang", + "旁" : "pang", + "螃" : "pang", + "耪" : "pang", + "抛" : "pao", + "咆" : "pao", + "庖" : "pao", + "袍" : "pao", + "跑" : "pao", + "泡" : "pao", + "呸" : "pei", + "胚" : "pei", + "陪" : "pei", + "培" : "pei", + "赔" : "pei", + "裴" : "pei", + "沛" : "pei", + "佩" : "pei", + "配" : "pei", + "喷" : "pen", + "盆" : "pen", + "抨" : "peng", + "怦" : "peng", + "砰" : "peng", + "烹" : "peng", + "嘭" : "peng", + "朋" : "peng", + "彭" : "peng", + "棚" : "peng", + "蓬" : "peng", + "硼" : "peng", + "鹏" : "peng", + "澎" : "peng", + "篷" : "peng", + "膨" : "peng", + "捧" : "peng", + "碰" : "peng", + "丕" : "pi", + "批" : "pi", + "纰" : "pi", + "坯" : "pi", + "披" : "pi", + "砒" : "pi", + "劈" : "pi", + "噼" : "pi", + "霹" : "pi", + "皮" : "pi", + "枇" : "pi", + "毗" : "pi", + "蚍" : "pi", + "疲" : "pi", + "啤" : "pi", + "琵" : "pi", + "脾" : "pi", + "貔" : "pi", + "匹" : "pi", + "痞" : "pi", + "癖" : "pi", + "屁" : "pi", + "睥" : "pi", + "媲" : "pi", + "僻" : "pi", + "譬" : "pi", + "偏" : "pian", + "篇" : "pian", + "翩" : "pian", + "骈" : "pian", + "蹁" : "pian", + "片" : "pian", + "骗" : "pian", + "剽" : "piao", + "漂" : "piao", + "飘" : "piao", + "瓢" : "piao", + "殍" : "piao", + "瞟" : "piao", + "票" : "piao", + "氕" : "pie", + "瞥" : "pie", + "撇" : "pie", + "拼" : "pin", + "姘" : "pin", + "贫" : "pin", + "频" : "pin", + "嫔" : "pin", + "颦" : "pin", + "品" : "pin", + "聘" : "pin", + "乒" : "ping", + "娉" : "ping", + "平" : "ping", + "评" : "ping", + "坪" : "ping", + "苹" : "ping", + "凭" : "ping", + "瓶" : "ping", + "萍" : "ping", + "钋" : "po", + "坡" : "po", + "泼" : "po", + "颇" : "po", + "婆" : "po", + "鄱" : "po", + "叵" : "po", + "珀" : "po", + "破" : "po", + "粕" : "po", + "魄" : "po", + "剖" : "pou", + "抔" : "pou", + "扑" : "pu", + "铺" : "pu", + "噗" : "pu", + "仆" : "pu", + "匍" : "pu", + "菩" : "pu", + "葡" : "pu", + "蒲" : "pu", + "璞" : "pu", + "圃" : "pu", + "浦" : "pu", + "普" : "pu", + "谱" : "pu", + "蹼" : "pu", + "七" : "qi", + "沏" : "qi", + "妻" : "qi", + "柒" : "qi", + "凄" : "qi", + "萋" : "qi", + "戚" : "qi", + "期" : "qi", + "欺" : "qi", + "嘁" : "qi", + "漆" : "qi", + "齐" : "qi", + "芪" : "qi", + "其" : "qi", + "歧" : "qi", + "祈" : "qi", + "祇" : "qi", + "脐" : "qi", + "畦" : "qi", + "跂" : "qi", + "崎" : "qi", + "骑" : "qi", + "琪" : "qi", + "棋" : "qi", + "旗" : "qi", + "鳍" : "qi", + "麒" : "qi", + "乞" : "qi", + "岂" : "qi", + "企" : "qi", + "杞" : "qi", + "启" : "qi", + "起" : "qi", + "绮" : "qi", + "气" : "qi", + "讫" : "qi", + "迄" : "qi", + "弃" : "qi", + "汽" : "qi", + "泣" : "qi", + "契" : "qi", + "砌" : "qi", + "葺" : "qi", + "器" : "qi", + "憩" : "qi", + "俟" : "qi", + "掐" : "qia", + "洽" : "qia", + "恰" : "qia", + "千" : "qian", + "仟" : "qian", + "阡" : "qian", + "芊" : "qian", + "迁" : "qian", + "钎" : "qian", + "牵" : "qian", + "悭" : "qian", + "谦" : "qian", + "签" : "qian", + "愆" : "qian", + "前" : "qian", + "虔" : "qian", + "钱" : "qian", + "钳" : "qian", + "乾" : "qian", + "潜" : "qian", + "黔" : "qian", + "遣" : "qian", + "谴" : "qian", + "欠" : "qian", + "芡" : "qian", + "倩" : "qian", + "堑" : "qian", + "嵌" : "qian", + "歉" : "qian", + "羌" : "qiang", + "枪" : "qiang", + "戕" : "qiang", + "腔" : "qiang", + "蜣" : "qiang", + "锵" : "qiang", + "墙" : "qiang", + "蔷" : "qiang", + "抢" : "qiang", + "羟" : "qiang", + "襁" : "qiang", + "呛" : "qiang", + "炝" : "qiang", + "跄" : "qiang", + "悄" : "qiao", + "跷" : "qiao", + "锹" : "qiao", + "敲" : "qiao", + "橇" : "qiao", + "乔" : "qiao", + "侨" : "qiao", + "荞" : "qiao", + "桥" : "qiao", + "憔" : "qiao", + "瞧" : "qiao", + "巧" : "qiao", + "俏" : "qiao", + "诮" : "qiao", + "峭" : "qiao", + "窍" : "qiao", + "翘" : "qiao", + "撬" : "qiao", + "切" : "qie", + "且" : "qie", + "妾" : "qie", + "怯" : "qie", + "窃" : "qie", + "挈" : "qie", + "惬" : "qie", + "趄" : "qie", + "锲" : "qie", + "钦" : "qin", + "侵" : "qin", + "衾" : "qin", + "芹" : "qin", + "芩" : "qin", + "秦" : "qin", + "琴" : "qin", + "禽" : "qin", + "勤" : "qin", + "擒" : "qin", + "噙" : "qin", + "寝" : "qin", + "沁" : "qin", + "青" : "qing", + "轻" : "qing", + "氢" : "qing", + "倾" : "qing", + "卿" : "qing", + "清" : "qing", + "蜻" : "qing", + "情" : "qing", + "晴" : "qing", + "氰" : "qing", + "擎" : "qing", + "顷" : "qing", + "请" : "qing", + "庆" : "qing", + "罄" : "qing", + "穷" : "qiong", + "穹" : "qiong", + "琼" : "qiong", + "丘" : "qiu", + "秋" : "qiu", + "蚯" : "qiu", + "鳅" : "qiu", + "囚" : "qiu", + "求" : "qiu", + "虬" : "qiu", + "泅" : "qiu", + "酋" : "qiu", + "球" : "qiu", + "遒" : "qiu", + "裘" : "qiu", + "岖" : "qu", + "驱" : "qu", + "屈" : "qu", + "蛆" : "qu", + "躯" : "qu", + "趋" : "qu", + "蛐" : "qu", + "黢" : "qu", + "渠" : "qu", + "瞿" : "qu", + "曲" : "qu", + "取" : "qu", + "娶" : "qu", + "龋" : "qu", + "去" : "qu", + "趣" : "qu", + "觑" : "qu", + "悛" : "quan", + "权" : "quan", + "全" : "quan", + "诠" : "quan", + "泉" : "quan", + "拳" : "quan", + "痊" : "quan", + "蜷" : "quan", + "醛" : "quan", + "犬" : "quan", + "劝" : "quan", + "券" : "quan", + "炔" : "que", + "缺" : "que", + "瘸" : "que", + "却" : "que", + "确" : "que", + "鹊" : "que", + "阙" : "que", + "榷" : "que", + "逡" : "qun", + "裙" : "qun", + "群" : "qun", + "蚺" : "ran", + "然" : "ran", + "燃" : "ran", + "冉" : "ran", + "苒" : "ran", + "染" : "ran", + "瓤" : "rang", + "壤" : "rang", + "攘" : "rang", + "嚷" : "rang", + "让" : "rang", + "荛" : "rao", + "饶" : "rao", + "娆" : "rao", + "桡" : "rao", + "扰" : "rao", + "绕" : "rao", + "惹" : "re", + "热" : "re", + "人" : "ren", + "壬" : "ren", + "仁" : "ren", + "忍" : "ren", + "荏" : "ren", + "稔" : "ren", + "刃" : "ren", + "认" : "ren", + "任" : "ren", + "纫" : "ren", + "韧" : "ren", + "饪" : "ren", + "扔" : "reng", + "仍" : "reng", + "日" : "ri", + "戎" : "rong", + "茸" : "rong", + "荣" : "rong", + "绒" : "rong", + "容" : "rong", + "嵘" : "rong", + "蓉" : "rong", + "溶" : "rong", + "榕" : "rong", + "熔" : "rong", + "融" : "rong", + "冗" : "rong", + "氄" : "rong", + "柔" : "rou", + "揉" : "rou", + "糅" : "rou", + "蹂" : "rou", + "鞣" : "rou", + "肉" : "rou", + "如" : "ru", + "茹" : "ru", + "铷" : "ru", + "儒" : "ru", + "孺" : "ru", + "蠕" : "ru", + "汝" : "ru", + "乳" : "ru", + "辱" : "ru", + "入" : "ru", + "缛" : "ru", + "褥" : "ru", + "阮" : "ruan", + "软" : "ruan", + "蕊" : "rui", + "蚋" : "rui", + "锐" : "rui", + "瑞" : "rui", + "睿" : "rui", + "闰" : "run", + "润" : "run", + "若" : "ruo", + "偌" : "ruo", + "弱" : "ruo", + "仨" : "sa", + "洒" : "sa", + "撒" : "sa", + "卅" : "sa", + "飒" : "sa", + "萨" : "sa", + "腮" : "sai", + "赛" : "sai", + "三" : "san", + "叁" : "san", + "伞" : "san", + "散" : "san", + "桑" : "sang", + "搡" : "sang", + "嗓" : "sang", + "丧" : "sang", + "搔" : "sao", + "骚" : "sao", + "扫" : "sao", + "嫂" : "sao", + "臊" : "sao", + "涩" : "se", + "啬" : "se", + "铯" : "se", + "瑟" : "se", + "穑" : "se", + "森" : "sen", + "僧" : "seng", + "杀" : "sha", + "沙" : "sha", + "纱" : "sha", + "砂" : "sha", + "啥" : "sha", + "傻" : "sha", + "厦" : "sha", + "歃" : "sha", + "煞" : "sha", + "霎" : "sha", + "筛" : "shai", + "晒" : "shai", + "山" : "shan", + "删" : "shan", + "苫" : "shan", + "衫" : "shan", + "姗" : "shan", + "珊" : "shan", + "煽" : "shan", + "潸" : "shan", + "膻" : "shan", + "闪" : "shan", + "陕" : "shan", + "讪" : "shan", + "汕" : "shan", + "扇" : "shan", + "善" : "shan", + "骟" : "shan", + "缮" : "shan", + "擅" : "shan", + "膳" : "shan", + "嬗" : "shan", + "赡" : "shan", + "鳝" : "shan", + "伤" : "shang", + "殇" : "shang", + "商" : "shang", + "觞" : "shang", + "熵" : "shang", + "晌" : "shang", + "赏" : "shang", + "上" : "shang", + "尚" : "shang", + "捎" : "shao", + "烧" : "shao", + "梢" : "shao", + "稍" : "shao", + "艄" : "shao", + "勺" : "shao", + "芍" : "shao", + "韶" : "shao", + "少" : "shao", + "邵" : "shao", + "绍" : "shao", + "哨" : "shao", + "潲" : "shao", + "奢" : "she", + "赊" : "she", + "舌" : "she", + "佘" : "she", + "蛇" : "she", + "舍" : "she", + "设" : "she", + "社" : "she", + "射" : "she", + "涉" : "she", + "赦" : "she", + "摄" : "she", + "慑" : "she", + "麝" : "she", + "申" : "shen", + "伸" : "shen", + "身" : "shen", + "呻" : "shen", + "绅" : "shen", + "砷" : "shen", + "深" : "shen", + "神" : "shen", + "沈" : "shen", + "审" : "shen", + "哂" : "shen", + "婶" : "shen", + "肾" : "shen", + "甚" : "shen", + "渗" : "shen", + "葚" : "shen", + "蜃" : "shen", + "慎" : "shen", + "升" : "sheng", + "生" : "sheng", + "声" : "sheng", + "昇" : "sheng", + "牲" : "sheng", + "笙" : "sheng", + "甥" : "sheng", + "绳" : "sheng", + "圣" : "sheng", + "胜" : "sheng", + "晟" : "sheng", + "剩" : "sheng", + "尸" : "shi", + "失" : "shi", + "师" : "shi", + "诗" : "shi", + "虱" : "shi", + "狮" : "shi", + "施" : "shi", + "湿" : "shi", + "十" : "shi", + "时" : "shi", + "实" : "shi", + "食" : "shi", + "蚀" : "shi", + "史" : "shi", + "矢" : "shi", + "使" : "shi", + "始" : "shi", + "驶" : "shi", + "屎" : "shi", + "士" : "shi", + "氏" : "shi", + "示" : "shi", + "世" : "shi", + "仕" : "shi", + "市" : "shi", + "式" : "shi", + "势" : "shi", + "事" : "shi", + "侍" : "shi", + "饰" : "shi", + "试" : "shi", + "视" : "shi", + "拭" : "shi", + "柿" : "shi", + "是" : "shi", + "适" : "shi", + "恃" : "shi", + "室" : "shi", + "逝" : "shi", + "轼" : "shi", + "舐" : "shi", + "弑" : "shi", + "释" : "shi", + "谥" : "shi", + "嗜" : "shi", + "誓" : "shi", + "收" : "shou", + "手" : "shou", + "守" : "shou", + "首" : "shou", + "寿" : "shou", + "受" : "shou", + "狩" : "shou", + "授" : "shou", + "售" : "shou", + "兽" : "shou", + "绶" : "shou", + "瘦" : "shou", + "殳" : "shu", + "书" : "shu", + "抒" : "shu", + "枢" : "shu", + "叔" : "shu", + "姝" : "shu", + "殊" : "shu", + "倏" : "shu", + "梳" : "shu", + "淑" : "shu", + "舒" : "shu", + "疏" : "shu", + "输" : "shu", + "蔬" : "shu", + "秫" : "shu", + "孰" : "shu", + "赎" : "shu", + "塾" : "shu", + "暑" : "shu", + "黍" : "shu", + "署" : "shu", + "蜀" : "shu", + "鼠" : "shu", + "薯" : "shu", + "曙" : "shu", + "戍" : "shu", + "束" : "shu", + "述" : "shu", + "树" : "shu", + "竖" : "shu", + "恕" : "shu", + "庶" : "shu", + "墅" : "shu", + "漱" : "shu", + "刷" : "shua", + "唰" : "shua", + "耍" : "shua", + "衰" : "shuai", + "摔" : "shuai", + "甩" : "shuai", + "帅" : "shuai", + "蟀" : "shuai", + "闩" : "shuan", + "拴" : "shuan", + "栓" : "shuan", + "涮" : "shuan", + "双" : "shuang", + "霜" : "shuang", + "孀" : "shuang", + "爽" : "shuang", + "谁" : "shui", + "水" : "shui", + "税" : "shui", + "睡" : "shui", + "吮" : "shun", + "顺" : "shun", + "舜" : "shun", + "瞬" : "shun", + "烁" : "shuo", + "铄" : "shuo", + "朔" : "shuo", + "硕" : "shuo", + "司" : "si", + "丝" : "si", + "私" : "si", + "咝" : "si", + "思" : "si", + "斯" : "si", + "厮" : "si", + "撕" : "si", + "嘶" : "si", + "死" : "si", + "巳" : "si", + "四" : "si", + "寺" : "si", + "祀" : "si", + "饲" : "si", + "肆" : "si", + "嗣" : "si", + "松" : "song", + "嵩" : "song", + "怂" : "song", + "耸" : "song", + "悚" : "song", + "讼" : "song", + "宋" : "song", + "送" : "song", + "诵" : "song", + "颂" : "song", + "搜" : "sou", + "嗖" : "sou", + "馊" : "sou", + "艘" : "sou", + "叟" : "sou", + "擞" : "sou", + "嗽" : "sou", + "苏" : "su", + "酥" : "su", + "俗" : "su", + "夙" : "su", + "诉" : "su", + "肃" : "su", + "素" : "su", + "速" : "su", + "粟" : "su", + "嗉" : "su", + "塑" : "su", + "溯" : "su", + "簌" : "su", + "酸" : "suan", + "蒜" : "suan", + "算" : "suan", + "虽" : "sui", + "睢" : "sui", + "绥" : "sui", + "隋" : "sui", + "随" : "sui", + "髓" : "sui", + "岁" : "sui", + "祟" : "sui", + "遂" : "sui", + "碎" : "sui", + "隧" : "sui", + "穗" : "sui", + "孙" : "sun", + "损" : "sun", + "笋" : "sun", + "隼" : "sun", + "唆" : "suo", + "梭" : "suo", + "蓑" : "suo", + "羧" : "suo", + "缩" : "suo", + "所" : "suo", + "索" : "suo", + "唢" : "suo", + "琐" : "suo", + "锁" : "suo", + "他" : "ta", + "它" : "ta", + "她" : "ta", + "铊" : "ta", + "塌" : "ta", + "塔" : "ta", + "獭" : "ta", + "挞" : "ta", + "榻" : "ta", + "踏" : "ta", + "蹋" : "ta", + "胎" : "tai", + "台" : "tai", + "邰" : "tai", + "抬" : "tai", + "苔" : "tai", + "跆" : "tai", + "太" : "tai", + "汰" : "tai", + "态" : "tai", + "钛" : "tai", + "泰" : "tai", + "酞" : "tai", + "贪" : "tan", + "摊" : "tan", + "滩" : "tan", + "瘫" : "tan", + "坛" : "tan", + "昙" : "tan", + "谈" : "tan", + "痰" : "tan", + "谭" : "tan", + "潭" : "tan", + "檀" : "tan", + "坦" : "tan", + "袒" : "tan", + "毯" : "tan", + "叹" : "tan", + "炭" : "tan", + "探" : "tan", + "碳" : "tan", + "汤" : "tang", + "嘡" : "tang", + "羰" : "tang", + "唐" : "tang", + "堂" : "tang", + "棠" : "tang", + "塘" : "tang", + "搪" : "tang", + "膛" : "tang", + "镗" : "tang", + "糖" : "tang", + "螳" : "tang", + "倘" : "tang", + "淌" : "tang", + "躺" : "tang", + "烫" : "tang", + "趟" : "tang", + "涛" : "tao", + "绦" : "tao", + "掏" : "tao", + "滔" : "tao", + "韬" : "tao", + "饕" : "tao", + "逃" : "tao", + "桃" : "tao", + "陶" : "tao", + "萄" : "tao", + "淘" : "tao", + "讨" : "tao", + "套" : "tao", + "特" : "te", + "疼" : "teng", + "腾" : "teng", + "誊" : "teng", + "滕" : "teng", + "藤" : "teng", + "剔" : "ti", + "梯" : "ti", + "踢" : "ti", + "啼" : "ti", + "题" : "ti", + "醍" : "ti", + "蹄" : "ti", + "体" : "ti", + "屉" : "ti", + "剃" : "ti", + "涕" : "ti", + "悌" : "ti", + "惕" : "ti", + "替" : "ti", + "天" : "tian", + "添" : "tian", + "田" : "tian", + "恬" : "tian", + "甜" : "tian", + "填" : "tian", + "忝" : "tian", + "殄" : "tian", + "舔" : "tian", + "掭" : "tian", + "佻" : "tiao", + "挑" : "tiao", + "条" : "tiao", + "迢" : "tiao", + "笤" : "tiao", + "髫" : "tiao", + "窕" : "tiao", + "眺" : "tiao", + "粜" : "tiao", + "跳" : "tiao", + "帖" : "tie", + "贴" : "tie", + "铁" : "tie", + "餮" : "tie", + "铤" : "ting", + "厅" : "ting", + "听" : "ting", + "烃" : "ting", + "廷" : "ting", + "亭" : "ting", + "庭" : "ting", + "停" : "ting", + "蜓" : "ting", + "婷" : "ting", + "霆" : "ting", + "挺" : "ting", + "艇" : "ting", + "通" : "tong", + "嗵" : "tong", + "同" : "tong", + "彤" : "tong", + "桐" : "tong", + "铜" : "tong", + "童" : "tong", + "潼" : "tong", + "瞳" : "tong", + "统" : "tong", + "捅" : "tong", + "桶" : "tong", + "筒" : "tong", + "恸" : "tong", + "痛" : "tong", + "偷" : "tou", + "头" : "tou", + "投" : "tou", + "骰" : "tou", + "透" : "tou", + "凸" : "tu", + "秃" : "tu", + "突" : "tu", + "图" : "tu", + "荼" : "tu", + "徒" : "tu", + "途" : "tu", + "涂" : "tu", + "屠" : "tu", + "土" : "tu", + "吐" : "tu", + "兔" : "tu", + "菟" : "tu", + "湍" : "tuan", + "团" : "tuan", + "疃" : "tuan", + "彖" : "tuan", + "推" : "tui", + "颓" : "tui", + "腿" : "tui", + "退" : "tui", + "蜕" : "tui", + "褪" : "tui", + "吞" : "tun", + "屯" : "tun", + "饨" : "tun", + "豚" : "tun", + "臀" : "tun", + "托" : "tuo", + "拖" : "tuo", + "脱" : "tuo", + "佗" : "tuo", + "陀" : "tuo", + "驼" : "tuo", + "鸵" : "tuo", + "妥" : "tuo", + "椭" : "tuo", + "唾" : "tuo", + "挖" : "wa", + "哇" : "wa", + "洼" : "wa", + "娲" : "wa", + "蛙" : "wa", + "娃" : "wa", + "瓦" : "wa", + "佤" : "wa", + "袜" : "wa", + "歪" : "wai", + "外" : "wai", + "弯" : "wan", + "剜" : "wan", + "湾" : "wan", + "蜿" : "wan", + "豌" : "wan", + "丸" : "wan", + "纨" : "wan", + "完" : "wan", + "玩" : "wan", + "顽" : "wan", + "烷" : "wan", + "宛" : "wan", + "挽" : "wan", + "晚" : "wan", + "惋" : "wan", + "婉" : "wan", + "绾" : "wan", + "皖" : "wan", + "碗" : "wan", + "万" : "wan", + "腕" : "wan", + "汪" : "wang", + "亡" : "wang", + "王" : "wang", + "网" : "wang", + "枉" : "wang", + "罔" : "wang", + "往" : "wang", + "惘" : "wang", + "妄" : "wang", + "忘" : "wang", + "旺" : "wang", + "望" : "wang", + "危" : "wei", + "威" : "wei", + "偎" : "wei", + "微" : "wei", + "煨" : "wei", + "薇" : "wei", + "巍" : "wei", + "韦" : "wei", + "为" : "wei", + "违" : "wei", + "围" : "wei", + "闱" : "wei", + "桅" : "wei", + "唯" : "wei", + "帷" : "wei", + "维" : "wei", + "伟" : "wei", + "伪" : "wei", + "苇" : "wei", + "纬" : "wei", + "委" : "wei", + "诿" : "wei", + "娓" : "wei", + "萎" : "wei", + "猥" : "wei", + "痿" : "wei", + "卫" : "wei", + "未" : "wei", + "位" : "wei", + "味" : "wei", + "畏" : "wei", + "胃" : "wei", + "谓" : "wei", + "喂" : "wei", + "猬" : "wei", + "渭" : "wei", + "蔚" : "wei", + "慰" : "wei", + "魏" : "wei", + "温" : "wen", + "瘟" : "wen", + "文" : "wen", + "纹" : "wen", + "闻" : "wen", + "蚊" : "wen", + "雯" : "wen", + "刎" : "wen", + "吻" : "wen", + "紊" : "wen", + "稳" : "wen", + "问" : "wen", + "汶" : "wen", + "翁" : "weng", + "嗡" : "weng", + "瓮" : "weng", + "挝" : "wo", + "莴" : "wo", + "倭" : "wo", + "喔" : "wo", + "窝" : "wo", + "蜗" : "wo", + "我" : "wo", + "肟" : "wo", + "沃" : "wo", + "卧" : "wo", + "握" : "wo", + "幄" : "wo", + "斡" : "wo", + "乌" : "wu", + "邬" : "wu", + "污" : "wu", + "巫" : "wu", + "呜" : "wu", + "钨" : "wu", + "诬" : "wu", + "屋" : "wu", + "无" : "wu", + "毋" : "wu", + "芜" : "wu", + "吴" : "wu", + "梧" : "wu", + "蜈" : "wu", + "五" : "wu", + "午" : "wu", + "伍" : "wu", + "仵" : "wu", + "怃" : "wu", + "忤" : "wu", + "妩" : "wu", + "武" : "wu", + "侮" : "wu", + "捂" : "wu", + "鹉" : "wu", + "舞" : "wu", + "兀" : "wu", + "勿" : "wu", + "戊" : "wu", + "务" : "wu", + "坞" : "wu", + "物" : "wu", + "误" : "wu", + "悟" : "wu", + "晤" : "wu", + "骛" : "wu", + "雾" : "wu", + "寤" : "wu", + "鹜" : "wu", + "夕" : "xi", + "兮" : "xi", + "西" : "xi", + "吸" : "xi", + "汐" : "xi", + "希" : "xi", + "昔" : "xi", + "析" : "xi", + "唏" : "xi", + "牺" : "xi", + "息" : "xi", + "奚" : "xi", + "悉" : "xi", + "烯" : "xi", + "惜" : "xi", + "晰" : "xi", + "稀" : "xi", + "翕" : "xi", + "犀" : "xi", + "皙" : "xi", + "锡" : "xi", + "溪" : "xi", + "熙" : "xi", + "蜥" : "xi", + "熄" : "xi", + "嘻" : "xi", + "膝" : "xi", + "嬉" : "xi", + "羲" : "xi", + "蟋" : "xi", + "曦" : "xi", + "习" : "xi", + "席" : "xi", + "袭" : "xi", + "媳" : "xi", + "洗" : "xi", + "玺" : "xi", + "徙" : "xi", + "喜" : "xi", + "禧" : "xi", + "戏" : "xi", + "细" : "xi", + "隙" : "xi", + "呷" : "xia", + "虾" : "xia", + "瞎" : "xia", + "匣" : "xia", + "侠" : "xia", + "峡" : "xia", + "狭" : "xia", + "遐" : "xia", + "瑕" : "xia", + "暇" : "xia", + "辖" : "xia", + "霞" : "xia", + "黠" : "xia", + "下" : "xia", + "夏" : "xia", + "罅" : "xia", + "仙" : "xian", + "先" : "xian", + "氙" : "xian", + "掀" : "xian", + "酰" : "xian", + "锨" : "xian", + "鲜" : "xian", + "闲" : "xian", + "贤" : "xian", + "弦" : "xian", + "咸" : "xian", + "涎" : "xian", + "娴" : "xian", + "衔" : "xian", + "舷" : "xian", + "嫌" : "xian", + "显" : "xian", + "险" : "xian", + "跣" : "xian", + "藓" : "xian", + "苋" : "xian", + "县" : "xian", + "现" : "xian", + "限" : "xian", + "线" : "xian", + "宪" : "xian", + "陷" : "xian", + "馅" : "xian", + "羡" : "xian", + "献" : "xian", + "腺" : "xian", + "乡" : "xiang", + "相" : "xiang", + "香" : "xiang", + "厢" : "xiang", + "湘" : "xiang", + "箱" : "xiang", + "襄" : "xiang", + "镶" : "xiang", + "详" : "xiang", + "祥" : "xiang", + "翔" : "xiang", + "享" : "xiang", + "响" : "xiang", + "饷" : "xiang", + "飨" : "xiang", + "想" : "xiang", + "向" : "xiang", + "项" : "xiang", + "象" : "xiang", + "像" : "xiang", + "橡" : "xiang", + "肖" : "xiao", + "枭" : "xiao", + "哓" : "xiao", + "骁" : "xiao", + "逍" : "xiao", + "消" : "xiao", + "宵" : "xiao", + "萧" : "xiao", + "硝" : "xiao", + "销" : "xiao", + "箫" : "xiao", + "潇" : "xiao", + "霄" : "xiao", + "魈" : "xiao", + "嚣" : "xiao", + "崤" : "xiao", + "淆" : "xiao", + "小" : "xiao", + "晓" : "xiao", + "孝" : "xiao", + "哮" : "xiao", + "笑" : "xiao", + "效" : "xiao", + "啸" : "xiao", + "挟" : "xie", + "些" : "xie", + "楔" : "xie", + "歇" : "xie", + "蝎" : "xie", + "协" : "xie", + "胁" : "xie", + "偕" : "xie", + "斜" : "xie", + "谐" : "xie", + "揳" : "xie", + "携" : "xie", + "撷" : "xie", + "鞋" : "xie", + "写" : "xie", + "泄" : "xie", + "泻" : "xie", + "卸" : "xie", + "屑" : "xie", + "械" : "xie", + "亵" : "xie", + "谢" : "xie", + "邂" : "xie", + "懈" : "xie", + "蟹" : "xie", + "心" : "xin", + "芯" : "xin", + "辛" : "xin", + "欣" : "xin", + "锌" : "xin", + "新" : "xin", + "歆" : "xin", + "薪" : "xin", + "馨" : "xin", + "鑫" : "xin", + "信" : "xin", + "衅" : "xin", + "星" : "xing", + "猩" : "xing", + "惺" : "xing", + "腥" : "xing", + "刑" : "xing", + "邢" : "xing", + "形" : "xing", + "型" : "xing", + "醒" : "xing", + "擤" : "xing", + "兴" : "xing", + "杏" : "xing", + "幸" : "xing", + "性" : "xing", + "姓" : "xing", + "悻" : "xing", + "凶" : "xiong", + "兄" : "xiong", + "匈" : "xiong", + "讻" : "xiong", + "汹" : "xiong", + "胸" : "xiong", + "雄" : "xiong", + "熊" : "xiong", + "休" : "xiu", + "咻" : "xiu", + "修" : "xiu", + "羞" : "xiu", + "朽" : "xiu", + "秀" : "xiu", + "袖" : "xiu", + "绣" : "xiu", + "锈" : "xiu", + "嗅" : "xiu", + "欻" : "xu", + "戌" : "xu", + "须" : "xu", + "胥" : "xu", + "虚" : "xu", + "墟" : "xu", + "需" : "xu", + "魆" : "xu", + "徐" : "xu", + "许" : "xu", + "诩" : "xu", + "栩" : "xu", + "旭" : "xu", + "序" : "xu", + "叙" : "xu", + "恤" : "xu", + "酗" : "xu", + "勖" : "xu", + "绪" : "xu", + "续" : "xu", + "絮" : "xu", + "婿" : "xu", + "蓄" : "xu", + "煦" : "xu", + "轩" : "xuan", + "宣" : "xuan", + "揎" : "xuan", + "喧" : "xuan", + "暄" : "xuan", + "玄" : "xuan", + "悬" : "xuan", + "旋" : "xuan", + "漩" : "xuan", + "璇" : "xuan", + "选" : "xuan", + "癣" : "xuan", + "炫" : "xuan", + "绚" : "xuan", + "眩" : "xuan", + "渲" : "xuan", + "靴" : "xue", + "薛" : "xue", + "穴" : "xue", + "学" : "xue", + "噱" : "xue", + "雪" : "xue", + "谑" : "xue", + "勋" : "xun", + "熏" : "xun", + "薰" : "xun", + "醺" : "xun", + "旬" : "xun", + "寻" : "xun", + "巡" : "xun", + "询" : "xun", + "荀" : "xun", + "循" : "xun", + "训" : "xun", + "讯" : "xun", + "汛" : "xun", + "迅" : "xun", + "驯" : "xun", + "徇" : "xun", + "逊" : "xun", + "殉" : "xun", + "巽" : "xun", + "丫" : "ya", + "压" : "ya", + "押" : "ya", + "鸦" : "ya", + "桠" : "ya", + "鸭" : "ya", + "牙" : "ya", + "伢" : "ya", + "芽" : "ya", + "蚜" : "ya", + "崖" : "ya", + "涯" : "ya", + "睚" : "ya", + "衙" : "ya", + "哑" : "ya", + "雅" : "ya", + "亚" : "ya", + "讶" : "ya", + "娅" : "ya", + "氩" : "ya", + "揠" : "ya", + "呀" : "ya", + "恹" : "yan", + "胭" : "yan", + "烟" : "yan", + "焉" : "yan", + "阉" : "yan", + "淹" : "yan", + "湮" : "yan", + "嫣" : "yan", + "延" : "yan", + "闫" : "yan", + "严" : "yan", + "言" : "yan", + "妍" : "yan", + "岩" : "yan", + "炎" : "yan", + "沿" : "yan", + "研" : "yan", + "盐" : "yan", + "阎" : "yan", + "蜒" : "yan", + "筵" : "yan", + "颜" : "yan", + "檐" : "yan", + "奄" : "yan", + "俨" : "yan", + "衍" : "yan", + "掩" : "yan", + "郾" : "yan", + "眼" : "yan", + "偃" : "yan", + "演" : "yan", + "魇" : "yan", + "鼹" : "yan", + "厌" : "yan", + "砚" : "yan", + "彦" : "yan", + "艳" : "yan", + "晏" : "yan", + "唁" : "yan", + "宴" : "yan", + "验" : "yan", + "谚" : "yan", + "堰" : "yan", + "雁" : "yan", + "焰" : "yan", + "滟" : "yan", + "餍" : "yan", + "燕" : "yan", + "赝" : "yan", + "央" : "yang", + "泱" : "yang", + "殃" : "yang", + "鸯" : "yang", + "秧" : "yang", + "扬" : "yang", + "羊" : "yang", + "阳" : "yang", + "杨" : "yang", + "佯" : "yang", + "疡" : "yang", + "徉" : "yang", + "洋" : "yang", + "仰" : "yang", + "养" : "yang", + "氧" : "yang", + "痒" : "yang", + "怏" : "yang", + "样" : "yang", + "恙" : "yang", + "烊" : "yang", + "漾" : "yang", + "幺" : "yao", + "夭" : "yao", + "吆" : "yao", + "妖" : "yao", + "腰" : "yao", + "邀" : "yao", + "爻" : "yao", + "尧" : "yao", + "肴" : "yao", + "姚" : "yao", + "窑" : "yao", + "谣" : "yao", + "摇" : "yao", + "徭" : "yao", + "遥" : "yao", + "瑶" : "yao", + "杳" : "yao", + "咬" : "yao", + "舀" : "yao", + "窈" : "yao", + "药" : "yao", + "要" : "yao", + "鹞" : "yao", + "耀" : "yao", + "耶" : "ye", + "掖" : "ye", + "椰" : "ye", + "噎" : "ye", + "爷" : "ye", + "揶" : "ye", + "也" : "ye", + "冶" : "ye", + "野" : "ye", + "业" : "ye", + "叶" : "ye", + "页" : "ye", + "曳" : "ye", + "夜" : "ye", + "液" : "ye", + "谒" : "ye", + "腋" : "ye", + "一" : "yi", + "伊" : "yi", + "衣" : "yi", + "医" : "yi", + "依" : "yi", + "咿" : "yi", + "揖" : "yi", + "壹" : "yi", + "漪" : "yi", + "噫" : "yi", + "仪" : "yi", + "夷" : "yi", + "饴" : "yi", + "宜" : "yi", + "咦" : "yi", + "贻" : "yi", + "姨" : "yi", + "胰" : "yi", + "移" : "yi", + "痍" : "yi", + "颐" : "yi", + "疑" : "yi", + "彝" : "yi", + "乙" : "yi", + "已" : "yi", + "以" : "yi", + "苡" : "yi", + "矣" : "yi", + "迤" : "yi", + "蚁" : "yi", + "倚" : "yi", + "椅" : "yi", + "旖" : "yi", + "乂" : "yi", + "亿" : "yi", + "义" : "yi", + "艺" : "yi", + "刈" : "yi", + "忆" : "yi", + "议" : "yi", + "屹" : "yi", + "亦" : "yi", + "异" : "yi", + "抑" : "yi", + "呓" : "yi", + "邑" : "yi", + "役" : "yi", + "译" : "yi", + "易" : "yi", + "诣" : "yi", + "绎" : "yi", + "驿" : "yi", + "轶" : "yi", + "弈" : "yi", + "奕" : "yi", + "疫" : "yi", + "羿" : "yi", + "益" : "yi", + "谊" : "yi", + "逸" : "yi", + "翌" : "yi", + "肄" : "yi", + "裔" : "yi", + "意" : "yi", + "溢" : "yi", + "缢" : "yi", + "毅" : "yi", + "薏" : "yi", + "翳" : "yi", + "臆" : "yi", + "翼" : "yi", + "因" : "yin", + "阴" : "yin", + "茵" : "yin", + "荫" : "yin", + "音" : "yin", + "姻" : "yin", + "铟" : "yin", + "喑" : "yin", + "愔" : "yin", + "吟" : "yin", + "垠" : "yin", + "银" : "yin", + "淫" : "yin", + "寅" : "yin", + "龈" : "yin", + "霪" : "yin", + "尹" : "yin", + "引" : "yin", + "蚓" : "yin", + "隐" : "yin", + "瘾" : "yin", + "印" : "yin", + "英" : "ying", + "莺" : "ying", + "婴" : "ying", + "嘤" : "ying", + "罂" : "ying", + "缨" : "ying", + "樱" : "ying", + "鹦" : "ying", + "膺" : "ying", + "鹰" : "ying", + "迎" : "ying", + "茔" : "ying", + "荧" : "ying", + "盈" : "ying", + "莹" : "ying", + "萤" : "ying", + "营" : "ying", + "萦" : "ying", + "楹" : "ying", + "蝇" : "ying", + "赢" : "ying", + "瀛" : "ying", + "颍" : "ying", + "颖" : "ying", + "影" : "ying", + "应" : "ying", + "映" : "ying", + "硬" : "ying", + "哟" : "yo", + "唷" : "yo", + "佣" : "yong", + "拥" : "yong", + "庸" : "yong", + "雍" : "yong", + "壅" : "yong", + "臃" : "yong", + "永" : "yong", + "甬" : "yong", + "咏" : "yong", + "泳" : "yong", + "勇" : "yong", + "涌" : "yong", + "恿" : "yong", + "蛹" : "yong", + "踊" : "yong", + "用" : "yong", + "优" : "you", + "攸" : "you", + "忧" : "you", + "呦" : "you", + "幽" : "you", + "悠" : "you", + "尤" : "you", + "由" : "you", + "邮" : "you", + "犹" : "you", + "油" : "you", + "铀" : "you", + "鱿" : "you", + "游" : "you", + "友" : "you", + "有" : "you", + "酉" : "you", + "莠" : "you", + "黝" : "you", + "又" : "you", + "右" : "you", + "幼" : "you", + "佑" : "you", + "柚" : "you", + "囿" : "you", + "诱" : "you", + "鼬" : "you", + "迂" : "yu", + "纡" : "yu", + "於" : "yu", + "淤" : "yu", + "瘀" : "yu", + "于" : "yu", + "余" : "yu", + "盂" : "yu", + "臾" : "yu", + "鱼" : "yu", + "竽" : "yu", + "俞" : "yu", + "狳" : "yu", + "谀" : "yu", + "娱" : "yu", + "渔" : "yu", + "隅" : "yu", + "揄" : "yu", + "逾" : "yu", + "腴" : "yu", + "渝" : "yu", + "愉" : "yu", + "瑜" : "yu", + "榆" : "yu", + "虞" : "yu", + "愚" : "yu", + "舆" : "yu", + "与" : "yu", + "予" : "yu", + "屿" : "yu", + "宇" : "yu", + "羽" : "yu", + "雨" : "yu", + "禹" : "yu", + "语" : "yu", + "圄" : "yu", + "玉" : "yu", + "驭" : "yu", + "芋" : "yu", + "妪" : "yu", + "郁" : "yu", + "育" : "yu", + "狱" : "yu", + "浴" : "yu", + "预" : "yu", + "域" : "yu", + "欲" : "yu", + "谕" : "yu", + "遇" : "yu", + "喻" : "yu", + "御" : "yu", + "寓" : "yu", + "裕" : "yu", + "愈" : "yu", + "誉" : "yu", + "豫" : "yu", + "鹬" : "yu", + "鸢" : "yuan", + "鸳" : "yuan", + "冤" : "yuan", + "渊" : "yuan", + "元" : "yuan", + "园" : "yuan", + "垣" : "yuan", + "袁" : "yuan", + "原" : "yuan", + "圆" : "yuan", + "援" : "yuan", + "媛" : "yuan", + "缘" : "yuan", + "猿" : "yuan", + "源" : "yuan", + "辕" : "yuan", + "远" : "yuan", + "苑" : "yuan", + "怨" : "yuan", + "院" : "yuan", + "愿" : "yuan", + "曰" : "yue", + "月" : "yue", + "岳" : "yue", + "钺" : "yue", + "阅" : "yue", + "悦" : "yue", + "跃" : "yue", + "越" : "yue", + "粤" : "yue", + "晕" : "yun", + "云" : "yun", + "匀" : "yun", + "芸" : "yun", + "纭" : "yun", + "耘" : "yun", + "允" : "yun", + "陨" : "yun", + "殒" : "yun", + "孕" : "yun", + "运" : "yun", + "酝" : "yun", + "愠" : "yun", + "韵" : "yun", + "蕴" : "yun", + "熨" : "yun", + "匝" : "za", + "咂" : "za", + "杂" : "za", + "砸" : "za", + "灾" : "zai", + "甾" : "zai", + "哉" : "zai", + "栽" : "zai", + "载" : "zai", + "宰" : "zai", + "崽" : "zai", + "再" : "zai", + "在" : "zai", + "糌" : "zan", + "簪" : "zan", + "咱" : "zan", + "趱" : "zan", + "暂" : "zan", + "錾" : "zan", + "赞" : "zan", + "赃" : "zang", + "脏" : "zang", + "臧" : "zang", + "驵" : "zang", + "葬" : "zang", + "遭" : "zao", + "糟" : "zao", + "凿" : "zao", + "早" : "zao", + "枣" : "zao", + "蚤" : "zao", + "澡" : "zao", + "藻" : "zao", + "皂" : "zao", + "灶" : "zao", + "造" : "zao", + "噪" : "zao", + "燥" : "zao", + "躁" : "zao", + "则" : "ze", + "责" : "ze", + "泽" : "ze", + "啧" : "ze", + "帻" : "ze", + "仄" : "ze", + "贼" : "zei", + "怎" : "zen", + "谮" : "zen", + "增" : "zeng", + "憎" : "zeng", + "锃" : "zeng", + "赠" : "zeng", + "甑" : "zeng", + "吒" : "zha", + "挓" : "zha", + "哳" : "zha", + "揸" : "zha", + "渣" : "zha", + "楂" : "zha", + "札" : "zha", + "闸" : "zha", + "铡" : "zha", + "眨" : "zha", + "砟" : "zha", + "乍" : "zha", + "诈" : "zha", + "咤" : "zha", + "炸" : "zha", + "蚱" : "zha", + "榨" : "zha", + "拃" : "zha", + "斋" : "zhai", + "摘" : "zhai", + "宅" : "zhai", + "窄" : "zhai", + "债" : "zhai", + "砦" : "zhai", + "寨" : "zhai", + "沾" : "zhan", + "毡" : "zhan", + "粘" : "zhan", + "詹" : "zhan", + "谵" : "zhan", + "瞻" : "zhan", + "斩" : "zhan", + "盏" : "zhan", + "展" : "zhan", + "崭" : "zhan", + "搌" : "zhan", + "辗" : "zhan", + "占" : "zhan", + "栈" : "zhan", + "战" : "zhan", + "站" : "zhan", + "绽" : "zhan", + "湛" : "zhan", + "蘸" : "zhan", + "张" : "zhang", + "章" : "zhang", + "獐" : "zhang", + "彰" : "zhang", + "樟" : "zhang", + "蟑" : "zhang", + "涨" : "zhang", + "掌" : "zhang", + "丈" : "zhang", + "仗" : "zhang", + "杖" : "zhang", + "帐" : "zhang", + "账" : "zhang", + "胀" : "zhang", + "障" : "zhang", + "嶂" : "zhang", + "瘴" : "zhang", + "钊" : "zhao", + "招" : "zhao", + "昭" : "zhao", + "找" : "zhao", + "沼" : "zhao", + "兆" : "zhao", + "诏" : "zhao", + "赵" : "zhao", + "照" : "zhao", + "罩" : "zhao", + "肇" : "zhao", + "蜇" : "zhe", + "遮" : "zhe", + "哲" : "zhe", + "辄" : "zhe", + "蛰" : "zhe", + "谪" : "zhe", + "辙" : "zhe", + "者" : "zhe", + "锗" : "zhe", + "赭" : "zhe", + "褶" : "zhe", + "浙" : "zhe", + "蔗" : "zhe", + "鹧" : "zhe", + "贞" : "zhen", + "针" : "zhen", + "侦" : "zhen", + "珍" : "zhen", + "帧" : "zhen", + "胗" : "zhen", + "真" : "zhen", + "砧" : "zhen", + "斟" : "zhen", + "甄" : "zhen", + "榛" : "zhen", + "箴" : "zhen", + "臻" : "zhen", + "诊" : "zhen", + "枕" : "zhen", + "疹" : "zhen", + "缜" : "zhen", + "阵" : "zhen", + "鸩" : "zhen", + "振" : "zhen", + "朕" : "zhen", + "赈" : "zhen", + "震" : "zhen", + "镇" : "zhen", + "争" : "zheng", + "征" : "zheng", + "怔" : "zheng", + "峥" : "zheng", + "狰" : "zheng", + "睁" : "zheng", + "铮" : "zheng", + "筝" : "zheng", + "蒸" : "zheng", + "拯" : "zheng", + "整" : "zheng", + "正" : "zheng", + "证" : "zheng", + "郑" : "zheng", + "诤" : "zheng", + "政" : "zheng", + "挣" : "zheng", + "症" : "zheng", + "之" : "zhi", + "支" : "zhi", + "只" : "zhi", + "汁" : "zhi", + "芝" : "zhi", + "吱" : "zhi", + "枝" : "zhi", + "知" : "zhi", + "肢" : "zhi", + "织" : "zhi", + "栀" : "zhi", + "脂" : "zhi", + "蜘" : "zhi", + "执" : "zhi", + "直" : "zhi", + "侄" : "zhi", + "值" : "zhi", + "职" : "zhi", + "植" : "zhi", + "跖" : "zhi", + "踯" : "zhi", + "止" : "zhi", + "旨" : "zhi", + "址" : "zhi", + "芷" : "zhi", + "纸" : "zhi", + "祉" : "zhi", + "指" : "zhi", + "枳" : "zhi", + "咫" : "zhi", + "趾" : "zhi", + "酯" : "zhi", + "至" : "zhi", + "志" : "zhi", + "豸" : "zhi", + "帜" : "zhi", + "制" : "zhi", + "质" : "zhi", + "炙" : "zhi", + "治" : "zhi", + "栉" : "zhi", + "峙" : "zhi", + "挚" : "zhi", + "桎" : "zhi", + "致" : "zhi", + "秩" : "zhi", + "掷" : "zhi", + "痔" : "zhi", + "窒" : "zhi", + "蛭" : "zhi", + "智" : "zhi", + "痣" : "zhi", + "滞" : "zhi", + "置" : "zhi", + "雉" : "zhi", + "稚" : "zhi", + "中" : "zhong", + "忠" : "zhong", + "终" : "zhong", + "盅" : "zhong", + "钟" : "zhong", + "衷" : "zhong", + "肿" : "zhong", + "冢" : "zhong", + "踵" : "zhong", + "仲" : "zhong", + "众" : "zhong", + "舟" : "zhou", + "州" : "zhou", + "诌" : "zhou", + "周" : "zhou", + "洲" : "zhou", + "粥" : "zhou", + "妯" : "zhou", + "轴" : "zhou", + "肘" : "zhou", + "纣" : "zhou", + "咒" : "zhou", + "宙" : "zhou", + "胄" : "zhou", + "昼" : "zhou", + "皱" : "zhou", + "骤" : "zhou", + "帚" : "zhou", + "朱" : "zhu", + "侏" : "zhu", + "诛" : "zhu", + "茱" : "zhu", + "珠" : "zhu", + "株" : "zhu", + "诸" : "zhu", + "铢" : "zhu", + "猪" : "zhu", + "蛛" : "zhu", + "竹" : "zhu", + "竺" : "zhu", + "逐" : "zhu", + "烛" : "zhu", + "躅" : "zhu", + "主" : "zhu", + "拄" : "zhu", + "煮" : "zhu", + "嘱" : "zhu", + "瞩" : "zhu", + "伫" : "zhu", + "苎" : "zhu", + "助" : "zhu", + "住" : "zhu", + "贮" : "zhu", + "注" : "zhu", + "驻" : "zhu", + "柱" : "zhu", + "祝" : "zhu", + "著" : "zhu", + "蛀" : "zhu", + "铸" : "zhu", + "筑" : "zhu", + "抓" : "zhua", + "跩" : "zhuai", + "拽" : "zhuai", + "专" : "zhuan", + "砖" : "zhuan", + "转" : "zhuan", + "啭" : "zhuan", + "撰" : "zhuan", + "篆" : "zhuan", + "妆" : "zhuang", + "庄" : "zhuang", + "桩" : "zhuang", + "装" : "zhuang", + "壮" : "zhuang", + "状" : "zhuang", + "撞" : "zhuang", + "幢" : "zhuang", + "追" : "zhui", + "骓" : "zhui", + "锥" : "zhui", + "坠" : "zhui", + "缀" : "zhui", + "惴" : "zhui", + "赘" : "zhui", + "谆" : "zhun", + "准" : "zhun", + "拙" : "zhuo", + "捉" : "zhuo", + "桌" : "zhuo", + "灼" : "zhuo", + "茁" : "zhuo", + "卓" : "zhuo", + "斫" : "zhuo", + "浊" : "zhuo", + "酌" : "zhuo", + "啄" : "zhuo", + "擢" : "zhuo", + "镯" : "zhuo", + "孜" : "zi", + "咨" : "zi", + "姿" : "zi", + "赀" : "zi", + "资" : "zi", + "辎" : "zi", + "嗞" : "zi", + "滋" : "zi", + "锱" : "zi", + "龇" : "zi", + "子" : "zi", + "姊" : "zi", + "秭" : "zi", + "籽" : "zi", + "梓" : "zi", + "紫" : "zi", + "訾" : "zi", + "滓" : "zi", + "自" : "zi", + "字" : "zi", + "恣" : "zi", + "眦" : "zi", + "渍" : "zi", + "宗" : "zong", + "综" : "zong", + "棕" : "zong", + "踪" : "zong", + "鬃" : "zong", + "总" : "zong", + "纵" : "zong", + "粽" : "zong", + "邹" : "zou", + "走" : "zou", + "奏" : "zou", + "揍" : "zou", + "租" : "zu", + "足" : "zu", + "卒" : "zu", + "族" : "zu", + "诅" : "zu", + "阻" : "zu", + "组" : "zu", + "俎" : "zu", + "祖" : "zu", + "纂" : "zuan", + "钻" : "zuan", + "攥" : "zuan", + "嘴" : "zui", + "最" : "zui", + "罪" : "zui", + "醉" : "zui", + "尊" : "zun", + "遵" : "zun", + "樽" : "zun", + "鳟" : "zun", + "昨" : "zuo", + "左" : "zuo", + "佐" : "zuo", + "作" : "zuo", + "坐" : "zuo", + "阼" : "zuo", + "怍" : "zuo", + "祚" : "zuo", + "唑" : "zuo", + "座" : "zuo", + "做" : "zuo", + "酢" : "zuo", + "斌" : "bin", + "曾" : "zeng", + "查" : "zha", + "査" : "zha", + "乘" : "cheng", + "传" : "chuan", + "丁" : "ding", + "行" : "xing", + "瑾" : "jin", + "婧" : "jing", + "恺" : "kai", + "阚" : "kan", + "奎" : "kui", + "乐" : "le", + "陆" : "lu", + "逯" : "lv", + "璐" : "lu", + "淼" : "miao", + "闵" : "min", + "娜" : "na", + "奇" : "qi", + "琦" : "qi", + "强" : "qiang", + "邱" : "qiu", + "芮" : "rui", + "莎" : "sha", + "盛" : "sheng", + "石" : "shi", + "祎" : "yi", + "殷" : "yin", + "瑛" : "ying", + "昱" : "yu", + "眃" : "yun", + "琢" : "zhuo", + "枰" : "ping", + "玟" : "min", + "珉" : "min", + "珣" : "xun", + "淇" : "qi", + "缈" : "miao", + "彧" : "yu", + "祺" : "qi", + "骞" : "qian", + "垚" : "yao", + "妸" : "e", + "烜" : "hui", + "祁" : "qi", + "傢" : "jia", + "珮" : "pei", + "濮" : "pu", + "屺" : "qi", + "珅" : "shen", + "缇" : "ti", + "霈" : "pei", + "晞" : "xi", + "璠" : "fan", + "骐" : "qi", + "姞" : "ji", + "偲" : "cai", + "齼" : "chu", + "宓" : "mi", + "朴" : "pu", + "萁" : "qi", + "颀" : "qi", + "阗" : "tian", + "湉" : "tian", + "翀" : "chong", + "岷" : "min", + "桤" : "qi", + "囯" : "guo", + "浛" : "han", + "勐" : "meng", + "苠" : "min", + "岍" : "qian", + "皞" : "hao", + "岐" : "qi", + "溥" : "pu", + "锘" : "muo", + "渼" : "mei", + "燊" : "shen", + "玚" : "chang", + "亓" : "qi", + "湋" : "wei", + "涴" : "wan", + "沤" : "ou", + "胖" : "pang", + "莆" : "pu", + "扦" : "qian", + "僳" : "su", + "坍" : "tan", + "锑" : "ti", + "嚏" : "ti", + "腆" : "tian", + "丿" : "pie", + "鼗" : "tao", + "芈" : "mi", + "匚" : "fang", + "刂" : "li", + "冂" : "tong", + "亻" : "dan", + "仳" : "pi", + "俜" : "ping", + "俳" : "pai", + "倜" : "ti", + "傥" : "tang", + "傩" : "nuo", + "佥" : "qian", + "勹" : "bao", + "亠" : "tou", + "廾" : "gong", + "匏" : "pao", + "扌" : "ti", + "拚" : "pin", + "掊" : "pou", + "搦" : "nuo", + "擗" : "pi", + "啕" : "tao", + "嗦" : "suo", + "嗍" : "suo", + "辔" : "pei", + "嘌" : "piao", + "嗾" : "sou", + "嘧" : "mi", + "帔" : "pei", + "帑" : "tang", + "彡" : "san", + "犭" : "fan", + "狍" : "pao", + "狲" : "sun", + "狻" : "jun", + "飧" : "sun", + "夂" : "zhi", + "饣" : "shi", + "庀" : "pi", + "忄" : "shu", + "愫" : "su", + "闼" : "ta", + "丬" : "jiang", + "氵" : "san", + "汔" : "qi", + "沔" : "mian", + "汨" : "mi", + "泮" : "pan", + "洮" : "tao", + "涑" : "su", + "淠" : "pi", + "湓" : "pen", + "溻" : "ta", + "溏" : "tang", + "濉" : "sui", + "宀" : "bao", + "搴" : "qian", + "辶" : "zou", + "逄" : "pang", + "逖" : "ti", + "遢" : "ta", + "邈" : "miao", + "邃" : "sui", + "彐" : "ji", + "屮" : "cao", + "娑" : "suo", + "嫖" : "piao", + "纟" : "jiao", + "缗" : "min", + "瑭" : "tang", + "杪" : "miao", + "桫" : "suo", + "榀" : "pin", + "榫" : "sun", + "槭" : "qi", + "甓" : "pi", + "攴" : "po", + "耆" : "qi", + "牝" : "pin", + "犏" : "pian", + "氆" : "pu", + "攵" : "fan", + "肽" : "tai", + "胼" : "pian", + "脒" : "mi", + "脬" : "pao", + "旆" : "pei", + "炱" : "tai", + "燧" : "sui", + "灬" : "biao", + "礻" : "shi", + "祧" : "tiao", + "忑" : "te", + "忐" : "tan", + "愍" : "min", + "肀" : "yu", + "碛" : "qi", + "眄" : "mian", + "眇" : "miao", + "眭" : "sui", + "睃" : "suo", + "瞍" : "sou", + "畋" : "tian", + "罴" : "pi", + "蠓" : "meng", + "蠛" : "mie", + "笸" : "po", + "筢" : "pa", + "衄" : "nv", + "艋" : "meng", + "敉" : "mi", + "糸" : "mi", + "綦" : "qi", + "醅" : "pei", + "醣" : "tang", + "趿" : "ta", + "觫" : "su", + "龆" : "tiao", + "鲆" : "ping", + "稣" : "su", + "鲐" : "tai", + "鲦" : "tiao", + "鳎" : "ta", + "髂" : "qia", + "縻" : "mi", + "裒" : "pou", + "冫" : "liang", + "冖" : "tu", + "讠" : "yan", + "谇" : "sui", + "谝" : "pian", + "谡" : "su", + "卩" : "dan", + "阝" : "zuo", + "陴" : "pi", + "邳" : "pi", + "郫" : "pi", + "郯" : "tan", + "廴" : "yin", + "凵" : "qian", + "圮" : "pi", + "堋" : "peng", + "鼙" : "pi", + "艹" : "cao", + "芑" : "qi", + "苤" : "pie", + "荪" : "sun", + "荽" : "sui", + "葜" : "qia", + "蒎" : "pai", + "蔌" : "su", + "蕲" : "qi", + "薮" : "sou", + "薹" : "tai", + "蘼" : "mi", + "钅" : "jin", + "钷" : "po", + "钽" : "tan", + "铍" : "pi", + "铴" : "tang", + "铽" : "te", + "锫" : "pei", + "锬" : "tan", + "锼" : "sou", + "镤" : "pu", + "镨" : "pu", + "皤" : "po", + "鹈" : "ti", + "鹋" : "miao", + "疒" : "bing", + "疱" : "pao", + "衤" : "yi", + "袢" : "pan", + "裼" : "ti", + "襻" : "pan", + "耥" : "tang", + "耦" : "ou", + "虍" : "hu", + "蛴" : "qi", + "蜞" : "qi", + "蜱" : "pi", + "螋" : "sou", + "螗" : "tang", + "螵" : "piao", + "蟛" : "peng" +} diff --git a/kirby/i18n/translations/bg.json b/kirby/i18n/translations/bg.json new file mode 100755 index 0000000..e76e81c --- /dev/null +++ b/kirby/i18n/translations/bg.json @@ -0,0 +1,481 @@ +{ + "add": "\u0414\u043e\u0431\u0430\u0432\u0438", + "avatar": "Профилна снимка", + "back": "Назад", + "cancel": "\u041e\u0442\u043a\u0430\u0436\u0438", + "change": "\u041f\u0440\u043e\u043c\u0435\u043d\u0438", + "close": "\u0417\u0430\u0442\u0432\u043e\u0440\u0438", + "confirm": "Ок", + "copy": "Копирай", + "create": "Създай", + + "date": "Дата", + "date.select": "Select a date", + + "day": "Day", + "days.fri": "\u041f\u0442", + "days.mon": "\u041f\u043d", + "days.sat": "\u0421\u0431", + "days.sun": "\u041d\u0434", + "days.thu": "\u0427\u0442", + "days.tue": "\u0412\u0442", + "days.wed": "\u0421\u0440", + + "delete": "\u0418\u0437\u0442\u0440\u0438\u0439", + "dimensions": "Размери", + "disabled": "Disabled", + "discard": "\u041e\u0442\u043c\u0435\u043d\u0438", + "download": "Download", + "duplicate": "Duplicate", + "edit": "\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0430\u0439", + + "dialog.files.empty": "No files to select", + "dialog.pages.empty": "No pages to select", + "dialog.users.empty": "No users to select", + + "email": "Email", + "email.placeholder": "mail@example.com", + + "error.access.login": "Invalid login", + "error.access.panel": "Нямате права за достъп до панела", + "error.access.view": "You are not allowed to access this part of the panel", + + "error.avatar.create.fail": "Профилната снимка не може да се качи", + "error.avatar.delete.fail": "Профилната снимка не може да бъде изтрита", + "error.avatar.dimensions.invalid": + "Моля запазете ширината и височината на профилната снимка под 3000 пиксела", + "error.avatar.mime.forbidden": + "Профилната снимка трябва да бъде в JPEG или PNG формат", + + "error.blueprint.notFound": "Образецът \"{name}\" не може да бъде зареден", + + "error.email.preset.notFound": "Email шаблонът \"{name}\" не може да бъде открит", + + "error.field.converter.invalid": "Невалиден конвертор \"{converter}\"", + + "error.file.changeName.empty": "The name must not be empty", + "error.file.changeName.permission": + "Не можете да смените името на \"{filename}\"", + "error.file.duplicate": "Файл с име \"{filename}\" вече съществува", + "error.file.extension.forbidden": + "Файловото разширение \"{extension}\" не е позволено", + "error.file.extension.missing": + "Липсва файлово разширение за файла \"{filename}\"", + "error.file.maxheight": "The height of the image must not exceed {height} pixels", + "error.file.maxsize": "The file is too large", + "error.file.maxwidth": "The width of the image must not exceed {width} pixels", + "error.file.mime.differs": + "Каченият файл трябва да бъде от същия mime тип \"{mime}\"", + "error.file.mime.forbidden": "The media type \"{mime}\" is not allowed", + "error.file.mime.invalid": "Invalid mime type: {mime}", + "error.file.mime.missing": + "The media type for \"{filename}\" cannot be detected", + "error.file.minheight": "The height of the image must be at least {height} pixels", + "error.file.minsize": "The file is too small", + "error.file.minwidth": "The width of the image must be at least {width} pixels", + "error.file.name.missing": "Името на файла е задължително", + "error.file.notFound": "Файлът \"{filename}\" не може да бъде намерен", + "error.file.orientation": "The orientation of the image must be \"{orientation}\"", + "error.file.type.forbidden": "Не е позволен ъплоуда на файлове от тип {type}", + "error.file.undefined": "\u0424\u0430\u0439\u043b\u044a\u0442 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d", + + "error.form.incomplete": "Моля коригирайте всички грешки във формата...", + "error.form.notSaved": "Формата не може да бъде запазена", + + "error.language.code": "Please enter a valid code for the language", + "error.language.duplicate": "The language already exists", + "error.language.name": "Please enter a valid name for the language", + + "error.license.format": "Please enter a valid license key", + "error.license.email": "Моля въведете валиден email адрес", + "error.license.verification": "The license could not be verified", + + "error.page.changeSlug.permission": + "Не можете да смените URL на \"{slug}\"", + "error.page.changeStatus.incomplete": + "Страницата съдържа грешки и не може да бъде публикувана", + "error.page.changeStatus.permission": + "Статусът на страницата не може да бъде променен", + "error.page.changeStatus.toDraft.invalid": + "Страницата \"{slug}\" не може да бъде променена в чернова", + "error.page.changeTemplate.invalid": + "Темплейтът за страница \"{slug}\" не може да бъде променен", + "error.page.changeTemplate.permission": + "Нямате права за да промените шаблона за \"{slug}\"", + "error.page.changeTitle.empty": "Заглавието е задължително", + "error.page.changeTitle.permission": + "Не можете да промените заглавието на \"{slug}\"", + "error.page.create.permission": "Не можете да създадете \"{slug}\"", + "error.page.delete": "Страницата \"{slug}\" не може да бъде изтрита", + "error.page.delete.confirm": "Моля въведете името на страницата, за да потвърдите", + "error.page.delete.hasChildren": + "Страницата има подстраници и не може да бъде изтрита", + "error.page.delete.permission": "Не можете да изтриете \"{slug}\"", + "error.page.draft.duplicate": + "Вече съществува чернова с URL-добавка \"{slug}\"", + "error.page.duplicate": + "Страница с URL-добавка \"{slug}\" вече съществува", + "error.page.duplicate.permission": "You are not allowed to duplicate \"{slug}\"", + "error.page.notFound": "Страницата \"{slug}\" не може да бъде намерена", + "error.page.num.invalid": + "Моля въведете валидно число за сортиране. Числата не трябва да са негативни.", + "error.page.slug.invalid": "Моля въведете валиден URL префикс", + "error.page.sort.permission": "Страницата \"{slug}\" не може да бъде сортирана", + "error.page.status.invalid": "Моля изберете валиден статус на страницата", + "error.page.undefined": "\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0430", + "error.page.update.permission": "Не можете да обновите \"{slug}\"", + + "error.section.files.max.plural": + "Не можете да добавяте повече от {max} файлa в секция \"{section}\"", + "error.section.files.max.singular": + "Не можете да добавяте повече от един файл в секция \"{section}\"", + "error.section.files.min.plural": + "The \"{section}\" section requires at least {min} files", + "error.section.files.min.singular": + "The \"{section}\" section requires at least one file", + + "error.section.pages.max.plural": + "Не можете да добавяте повече от {max} страници в секция \"{section}\"", + "error.section.pages.max.singular": + "Не можете да добавяте повече от една страница в секция \"{section}\"", + "error.section.pages.min.plural": + "The \"{section}\" section requires at least {min} pages", + "error.section.pages.min.singular": + "The \"{section}\" section requires at least one page", + + "error.section.notLoaded": "Секция \"{name}\" не може да бъде заредена", + "error.section.type.invalid": "Типът \"{type}\" на секция не е валиден", + + "error.site.changeTitle.empty": "Заглавието е задължително", + "error.site.changeTitle.permission": + "Не може да променяте заглавието на сайта", + "error.site.update.permission": "Нямате права за да обновите сайта", + + "error.template.default.notFound": "Стандартният шаблон не съществува", + + "error.user.changeEmail.permission": + "Нямате права да промените имейла на този потребител \"{name}\"", + "error.user.changeLanguage.permission": + "Нямате права да промените езика за този потребител \"{name}\"", + "error.user.changeName.permission": + "Нямате права да промените името на този потребител \"{name}\"", + "error.user.changePassword.permission": + "Нямате права да промените паролата за този потребител \"{name}\"", + "error.user.changeRole.lastAdmin": + "Ролята на последния администратор не може да бъде променена", + "error.user.changeRole.permission": + "Нямате права да промените ролята на този потребител \"{name}\"", + "error.user.changeRole.toAdmin": + "You are not allowed to promote someone to the admin role", + "error.user.create.permission": "Нямате права да създадете този потребител", + "error.user.delete": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u0442 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0438\u0437\u0442\u0440\u0438\u0442", + "error.user.delete.lastAdmin": "\u041d\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u0438\u0437\u0442\u0440\u0438\u0435\u0442\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u044f \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440", + "error.user.delete.lastUser": "Последният потребител не може да бъде изтрит", + "error.user.delete.permission": + "\u041d\u0435 \u0435 \u043f\u043e\u0437\u0432\u043e\u043b\u0435\u043d\u043e \u0434\u0430 \u0438\u0437\u0442\u0440\u0438\u0432\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b", + "error.user.duplicate": + "Потребител с имейл \"{email}\" вече съществува", + "error.user.email.invalid": "Моля въведете валиден email адрес", + "error.user.language.invalid": "Моля въведете валиден език", + "error.user.notFound": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u0442 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d.", + "error.user.password.invalid": + "Моля въведете валидна парола. Тя трабва да съдържа поне 8 символа.", + "error.user.password.notSame": "\u041c\u043e\u043b\u044f, \u043f\u043e\u0442\u0432\u044a\u0440\u0434\u0435\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430", + "error.user.password.undefined": "Потребителят няма парола", + "error.user.role.invalid": "Моля въведете валидна роля", + "error.user.update.permission": + "Нямате права да обновите този потребител \"{name}\"", + + "error.validation.accepted": "Моля потвърдете", + "error.validation.alpha": "Моля въвдете символи измежду a-z", + "error.validation.alphanum": + "Моля въвдете символи измежду a-z или цифри 0-9", + "error.validation.between": + "Моля въведете стойност между \"{min}\" и \"{max}\"", + "error.validation.boolean": "Моля потвърдете или откажете", + "error.validation.contains": + "Моля въведете стойност, която съдържа \"{needle}\"", + "error.validation.date": "Моля въведете валидна дата", + "error.validation.date.after": "Please enter a date after {date}", + "error.validation.date.before": "Please enter a date before {date}", + "error.validation.date.between": "Please enter a date between {min} and {max}", + "error.validation.denied": "Моля откажете", + "error.validation.different": "Стойността не трябва да е \"{other}\"", + "error.validation.email": "Моля въведете валиден email адрес", + "error.validation.endswith": "Стойността трябва да завършва с \"{end\"}", + "error.validation.filename": "Моля въведете валидно име на файла", + "error.validation.in": "Моля въведете едно от следните: ({in})", + "error.validation.integer": "Моля въведете валидно цяло число", + "error.validation.ip": "Моля въведете валиден IP адрес", + "error.validation.less": "Моля въведете стойност по-ниска от {max}", + "error.validation.match": "Стойността не съвпада с очаквания модел", + "error.validation.max": "Please enter a value equal to or lower than {max}", + "error.validation.maxlength": + "Моля въведете по-къса стойност. (макс. {max} символа)", + "error.validation.maxwords": "Моля въведете не повече от {max} дума(и)", + "error.validation.min": "Please enter a value equal to or greater than {min}", + "error.validation.minlength": + "Моля въведете по-дълга стойност. (мин. {min} символа)", + "error.validation.minwords": "Моля въведете поне {min} дума(и).", + "error.validation.more": "Моля въведете стойност по-висока от {min}", + "error.validation.notcontains": + "Моля въведете стойност, която не съдържа \"{needle}\"", + "error.validation.notin": + "Моля не въвеждайте нито едно от следните: ({notIn})", + "error.validation.option": "Моля изберете валидна опция", + "error.validation.num": "Моля въведете валидно число", + "error.validation.required": "Моля въведете нещо", + "error.validation.same": "Моля въведете \"{other}\"", + "error.validation.size": "Размерът на стойността трябва да бъде \"{size}\"", + "error.validation.startswith": "Стойността трябва да започва с \"{start}\"", + "error.validation.time": "Моля въведете валидно време", + "error.validation.url": "Моля въведете валиден URL", + + "field.required": "The field is required", + "field.files.empty": "Все още не са избрани файлове", + "field.pages.empty": "Все още не са избрани страници", + "field.structure.delete.confirm": "Сигурни ли сте, че искате да изтриете това вписване?", + "field.structure.empty": "Все още няма статии", + "field.users.empty": "Все още не са избрани потребители", + + "file.delete.confirm": + "Сигурни ли сте, че искате да изтриете
{filename}?", + + "files": "Файлове", + "files.empty": "Няма файлове", + + "hour": "Hour", + "insert": "\u0412\u043c\u044a\u043a\u043d\u0438", + "install": "Инсталирай", + + "installation": "Инсталация", + "installation.completed": "The panel has been installed", + "installation.disabled": "The panel installer is disabled on public servers by default. Please run the installer on a local machine or enable it with the panel.install option.", + "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.php": "Бъдете сигурни, че използвате PHP 7+", + "installation.issues.server": + "Kirby изисква Apache, Nginx или Caddy", + "installation.issues.sessions": "The /site/sessions folder does not exist or is not writable", + + "language": "\u0415\u0437\u0438\u043a", + "language.code": "Код", + "language.convert": "Направи по подразбиране", + "language.convert.confirm": + "

Сигурни ли сте, че искате да зададете {name} за език по подразбиране? Действието не може да бъде отменено.

В случай, че в {name} има непреведено съдържание, то части от сайта ви могат да останат празни.

", + "language.create": "Добавете нов език", + "language.delete.confirm": + "Сигурни ли сте, че искате да изтриете език {name}, включително всички негови преводи? Действието не може да бъде отменено!", + "language.deleted": "Езикът беше изтрит", + "language.direction": "Посока на четене", + "language.direction.ltr": "Отляво надясно", + "language.direction.rtl": "Отдясно наляво", + "language.locale": "PHP locale string", + "language.locale.warning": "You are using a custom locale set up. Please modify it in the language file in /site/languages", + "language.name": "Име", + "language.updated": "Езикът беше обновен", + + "languages": "Езици", + "languages.default": "Език по подразбиране", + "languages.empty": "Все още няма добавени езици", + "languages.secondary": "Второстепенни езици", + "languages.secondary.empty": "Все още няма второстепенни езици", + + "license": "\u041b\u0438\u0446\u0435\u043d\u0437 \u0437\u0430 Kirby", + "license.buy": "Купи лиценз", + "license.register": "Регистрирай", + "license.register.help": + "You received your license code after the purchase via email. Please copy and paste it to register.", + "license.register.label": "Please enter your license code", + "license.register.success": "Thank you for supporting Kirby", + "license.unregistered": "Това е нерегистрирана демо версия на Kirby", + + "link": "\u0412\u0440\u044a\u0437\u043a\u0430", + "link.text": "Текстова връзка", + + "loading": "Зареждане", + + "lock.unsaved": "Unsaved changes", + "lock.unsaved.empty": "There are no more unsaved changes", + "lock.isLocked": "Unsaved changes by {email}", + "lock.file.isLocked": "The file is currently being edited by {email} and cannot be changed.", + "lock.page.isLocked": "The page is currently being edited by {email} and cannot be changed.", + "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.remember": "Keep me logged in", + + "logout": "Изход", + + "menu": "Меню", + "meridiem": "AM/PM", + "mime": "Media Type", + "minutes": "Minutes", + + "month": "Month", + "months.april": "\u0410\u043f\u0440\u0438\u043b", + "months.august": "\u0410\u0432\u0433\u0443\u0441\u0442", + "months.december": "\u0414\u0435\u043a\u0435\u043c\u0432\u0440\u0438", + "months.february": "\u0424\u0435\u0432\u0440\u0443\u0430\u0440\u0438", + "months.january": "\u042f\u043d\u0443\u0430\u0440\u0438", + "months.july": "\u042e\u043b\u0438", + "months.june": "\u042e\u043d\u0438", + "months.march": "\u041c\u0430\u0440\u0442", + "months.may": "\u041c\u0430\u0439", + "months.november": "\u041d\u043e\u0435\u043c\u0432\u0440\u0438", + "months.october": "\u041e\u043a\u0442\u043e\u043c\u0432\u0440\u0438", + "months.september": "\u0421\u0435\u043f\u0442\u0435\u043c\u0432\u0440\u0438", + + "more": "Още", + "name": "Име", + "next": "Next", + "off": "off", + "on": "on", + "open": "Отвори", + "options": "Options", + + "orientation": "Ориентация", + "orientation.landscape": "Пейзаж", + "orientation.portrait": "Портрет", + "orientation.square": "Квадрат", + + "page.changeSlug": "\u041f\u0440\u043e\u043c\u0435\u043d\u0438 URL", + "page.changeSlug.fromTitle": "\u0421\u044a\u0437\u0434\u0430\u0439\u0442\u0435 \u043e\u0442 \u0437\u0430\u0433\u043b\u0430\u0432\u0438\u0435\u0442\u043e", + "page.changeStatus": "Промени статус", + "page.changeStatus.position": "Моля изберете позиция", + "page.changeStatus.select": "Изберете нов статус", + "page.changeTemplate": "Промени шаблон", + "page.delete.confirm": + "Сигурни ли сте, че искате да изтриете {title}?", + "page.delete.confirm.subpages": + "Тази страница има подстраници.
Всички подстраници също ще бъдат изтрити.", + "page.delete.confirm.title": "Въведи заглавие на страница за да потвърдиш", + "page.draft.create": "Създай чернова", + "page.duplicate.appendix": "Копирай", + "page.duplicate.files": "Copy files", + "page.duplicate.pages": "Copy pages", + "page.status": "Status", + "page.status.draft": "Чернова", + "page.status.draft.description": + "Страницата е в режим на чернова и е видима само за оторизирани редактори", + "page.status.listed": "Публично", + "page.status.listed.description": "Страницата е публична за всички", + "page.status.unlisted": "Скрит", + "page.status.unlisted.description": "Страницата е достъпна само чрез URL", + + "pages": "Страници", + "pages.empty": "Все още няма страници", + "pages.status.draft": "Drafts", + "pages.status.listed": "Published", + "pages.status.unlisted": "Скрит", + + "pagination.page": "Страница", + + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "pixel": "Пиксел", + "prev": "Previous", + "remove": "Премахни", + "rename": "Преименувай", + "replace": "\u0417\u0430\u043c\u0435\u0441\u0442\u0438", + "retry": "\u041e\u043f\u0438\u0442\u0430\u0439 \u043f\u0430\u043a", + "revert": "\u041e\u0442\u043c\u0435\u043d\u0438", + + "role": "\u0420\u043e\u043b\u044f", + "role.admin.description": "The admin has all rights", + "role.admin.title": "Admin", + "role.all": "Всички", + "role.empty": "Не съществуват потребители с тази роля", + "role.description.placeholder": "Липсва описание", + "role.nobody.description": "This is a fallback role without any permissions", + "role.nobody.title": "Nobody", + + "save": "\u0417\u0430\u043f\u0438\u0448\u0438", + "search": "Търси", + + "section.required": "The section is required", + + "select": "Избери", + "settings": "Настройки", + "size": "Размер", + "slug": "URL-\u0434\u043e\u0431\u0430\u0432\u043a\u0430", + "sort": "Сортирай", + "title": "Заглавие", + "template": "Образец", + "today": "Днес", + + "toolbar.button.code": "Код", + "toolbar.button.bold": "\u041f\u043e\u043b\u0443\u0447\u0435\u0440 \u0448\u0440\u0438\u0444\u0442", + "toolbar.button.email": "Email", + "toolbar.button.headings": "Заглавия", + "toolbar.button.heading.1": "Заглавие 1", + "toolbar.button.heading.2": "Заглавие 2", + "toolbar.button.heading.3": "Заглавие 3", + "toolbar.button.italic": "\u041d\u0430\u043a\u043b\u043e\u043d\u0435\u043d \u0448\u0440\u0438\u0444\u0442", + "toolbar.button.file": "Файл", + "toolbar.button.file.select": "Select a file", + "toolbar.button.file.upload": "Upload a file", + "toolbar.button.link": "\u0412\u0440\u044a\u0437\u043a\u0430", + "toolbar.button.ol": "Подреден списък", + "toolbar.button.ul": "Списък", + + "translation.author": "Kirby екип", + "translation.direction": "ltr", + "translation.name": "Български", + "translation.locale": "bg_BG", + + "upload": "Прикачи", + "upload.error.cantMove": "The uploaded file could not be moved", + "upload.error.cantWrite": "Failed to write file to disk", + "upload.error.default": "The file could not be uploaded", + "upload.error.extension": "File upload stopped by extension", + "upload.error.formSize": "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the form", + "upload.error.iniPostSize": "The uploaded file exceeds the post_max_size directive in php.ini", + "upload.error.iniSize": "The uploaded file exceeds the upload_max_filesize directive in php.ini", + "upload.error.noFile": "No file was uploaded", + "upload.error.noFiles": "No files were uploaded", + "upload.error.partial": "The uploaded file was only partially uploaded", + "upload.error.tmpDir": "Missing a temporary folder", + "upload.errors": "Грешка", + "upload.progress": "Uploading…", + + "url": "Url", + "url.placeholder": "https://example.com", + + "user": "Потребител", + "user.blueprint": + "Можете да дефинирате допълнителни секции и полета на форми за тази потребителска роля в /site/blueprints/users/{role}.yml", + "user.changeEmail": "Промени email", + "user.changeLanguage": "Промени език", + "user.changeName": "Преименувай този потребител", + "user.changePassword": "Промени парола", + "user.changePassword.new": "Нова парола", + "user.changePassword.new.confirm": "Потвърдете новата парола...", + "user.changeRole": "Променете роля", + "user.changeRole.select": "Изберете нова роля", + "user.create": "Добавете нов потребител", + "user.delete": "Изтрийте потребителя", + "user.delete.confirm": + "Сигурни ли сте, че искате да изтриете
{email}?", + + "users": "Потребители", + + "version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Kirby", + + "view.account": "\u0412\u0430\u0448\u0438\u044f \u0430\u043a\u0430\u0443\u043d\u0442", + "view.installation": "\u0418\u043d\u0441\u0442\u0430\u043b\u0430\u0446\u0438\u044f", + "view.settings": "Настройки", + "view.site": "Сайт", + "view.users": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0438", + + "welcome": "Добре дошли", + "year": "Year" +} diff --git a/kirby/i18n/translations/ca.json b/kirby/i18n/translations/ca.json new file mode 100755 index 0000000..433bee5 --- /dev/null +++ b/kirby/i18n/translations/ca.json @@ -0,0 +1,481 @@ +{ + "add": "Afegir", + "avatar": "Imatge del perfil", + "back": "Tornar", + "cancel": "Cancel\u00b7lar", + "change": "Canviar", + "close": "Tancar", + "confirm": "Ok", + "copy": "Copiar", + "create": "Crear", + + "date": "Data", + "date.select": "Selecciona una data", + + "day": "Dia", + "days.fri": "dv.", + "days.mon": "dl.", + "days.sat": "ds.", + "days.sun": "dg.", + "days.thu": "dj.", + "days.tue": "dt.", + "days.wed": "dc.", + + "delete": "Eliminar", + "dimensions": "Dimensions", + "disabled": "Desactivat", + "discard": "Descartar", + "download": "Descarregar", + "duplicate": "Duplicar", + "edit": "Editar", + + "dialog.files.empty": "No hi ha cap fitxer per seleccionar", + "dialog.pages.empty": "No hi ha cap pàgina per seleccionar", + "dialog.users.empty": "No hi ha cap usuari per seleccionar", + + "email": "Email", + "email.placeholder": "mail@exemple.com", + + "error.access.login": "Inici de sessió no vàlid", + "error.access.panel": "No tens permís per accedir al panell", + "error.access.view": "No tens accés a aquesta part del tauler", + + "error.avatar.create.fail": "No s'ha pogut carregar la imatge del perfil", + "error.avatar.delete.fail": "La imatge del perfil no s'ha pogut eliminar", + "error.avatar.dimensions.invalid": + "Mantingueu l'amplada i l'alçada de la imatge de perfil de menys de 3000 píxels", + "error.avatar.mime.forbidden": + "La imatge del perfil ha de ser fitxers JPEG o PNG", + + "error.blueprint.notFound": "No s'ha potgut carregar el blueprint \"{name}\"", + + "error.email.preset.notFound": "No es pot trobar la configuració de correu electrònic \"{name}\"", + + "error.field.converter.invalid": "Convertidor no vàlid \"{converter}\"", + + "error.file.changeName.empty": "El nom no pot estar buit", + "error.file.changeName.permission": + "No tens permís per canviar el nom de \"{filename}\"", + "error.file.duplicate": "Ja existeix un fitxer amb el nom \"{filename}\"", + "error.file.extension.forbidden": + "L'extensió de l'arxiu \"{extension}\" no està permesa", + "error.file.extension.missing": + "Falta l'extensió de l'arxiu \"{filename}\"", + "error.file.maxheight": "L'alçada de la imatge no ha de ser superior a {height} píxels", + "error.file.maxsize": "El fitxer és massa gran", + "error.file.maxwidth": "L'amplada de la imatge no ha de ser superior a {width} píxels", + "error.file.mime.differs": + "L'arxiu carregat ha ha de ser del mateix tipus de mime \"{mime}\"", + "error.file.mime.forbidden": "El tipus de mitjà \"{mime}\" no està permès", + "error.file.mime.invalid": "Mime type no vàlid: {mime}", + "error.file.mime.missing": + "El tipus de suport per a \"{filename}\" no es pot detectar", + "error.file.minheight": "L'alçada de la imatge ha de ser com a mínim de {height} píxels", + "error.file.minsize": "El fitxer és massa petit", + "error.file.minwidth": "L'amplada de la imatge ha de ser com a mínim de {width} píxels", + "error.file.name.missing": "El nom del fitxer no pot estar buit", + "error.file.notFound": "L'arxiu \"{filename}\" no s'ha trobat", + "error.file.orientation": "L’orientació de la imatge ha de ser \"{orientation}\"", + "error.file.type.forbidden": "No tens permís per penjar fitxers {type}", + "error.file.undefined": "L'arxiu no s'ha trobat", + + "error.form.incomplete": "Si us plau, corregeix els errors del formulari ...", + "error.form.notSaved": "No s'ha pogut desar el formulari", + + "error.language.code": "Introdueix un codi vàlid per a l’idioma", + "error.language.duplicate": "L'idioma ja existeix", + "error.language.name": "Introdueix un nom vàlid per a l'idioma", + + "error.license.format": "Introduïu una clau de llicència vàlida", + "error.license.email": "Si us plau, introdueix una adreça de correu electrònic vàlida", + "error.license.verification": "No s’ha pogut verificar la llicència", + + "error.page.changeSlug.permission": + "No teniu permís per canviar l'apèndix d'URL per a \"{slug}\"", + "error.page.changeStatus.incomplete": + "La pàgina té errors i no es pot publicar", + "error.page.changeStatus.permission": + "No es pot canviar l'estat d'aquesta pàgina", + "error.page.changeStatus.toDraft.invalid": + "La pàgina \"{slug}\" no es pot convertir en un esborrany", + "error.page.changeTemplate.invalid": + "La plantilla per a la pàgina \"{slug}\" no es pot canviar", + "error.page.changeTemplate.permission": + "No tens permís per canviar la plantilla per \"{slug}\"", + "error.page.changeTitle.empty": "El títol no pot estar buit", + "error.page.changeTitle.permission": + "No tens permís per canviar el títol de \"{slug}\"", + "error.page.create.permission": "No tens permís per crear \"{slug}\"", + "error.page.delete": "La pàgina \"{slug}\" no es pot esborrar", + "error.page.delete.confirm": "Si us plau, introdueix el títol de la pàgina per confirmar", + "error.page.delete.hasChildren": + "La pàgina té subpàgines i no es pot esborrar", + "error.page.delete.permission": "No tens permís per esborrar \"{slug}\"", + "error.page.draft.duplicate": + "Ja existeix un esborrany de pàgina amb l'apèndix d'URL \"{slug}\"", + "error.page.duplicate": + "Ja existeix una pàgina amb l'apèndix d'URL \"{slug}\"", + "error.page.duplicate.permission": "No tens permís per duplicar \"{slug}\"", + "error.page.notFound": "La pàgina \"{slug}\" no s'ha trobat", + "error.page.num.invalid": + "Si us plau, introdueix un número d 'ordenació vàlid. Els números no poden ser negatius.", + "error.page.slug.invalid": "Introduïu un prefix d'URL vàlid", + "error.page.sort.permission": "La pàgina \"{slug}\" no es pot ordenar", + "error.page.status.invalid": "Si us plau, estableix un estat de pàgina vàlid", + "error.page.undefined": "La p\u00e0gina no s'ha trobat", + "error.page.update.permission": "No tens permís per actualitzar \"{slug}\"", + + "error.section.files.max.plural": + "No has d'afegir més de {max} fitxers a la secció \"{section}\"", + "error.section.files.max.singular": + "No podeu afegir més d'un fitxer a la secció \"{section}\"", + "error.section.files.min.plural": + "La secció \"{section}\" requereix almenys {min} fitxer", + "error.section.files.min.singular": + "La secció \"{section}\" requereix almenys un fitxer", + + "error.section.pages.max.plural": + "No heu d'afegir més de {max} pàgines a la secció \"{section}\"", + "error.section.pages.max.singular": + "No podeu afegir més d'una pàgina a la secció \"{section}\"", + "error.section.pages.min.plural": + "La secció \"{section}\" requereix almenys {min} pàgines", + "error.section.pages.min.singular": + "La secció \"{section}\" requereix almenys una pàgina", + + "error.section.notLoaded": "No s'ha pogut carregar la secció \"{name}\"", + "error.section.type.invalid": "La secció tipus \"{type}\" no és vàlida", + + "error.site.changeTitle.empty": "El títol no pot estar buit", + "error.site.changeTitle.permission": + "No tens permís per canviar el títol del lloc web", + "error.site.update.permission": "No tens permís per actualitzar el lloc web", + + "error.template.default.notFound": "La plantilla predeterminada no existeix", + + "error.user.changeEmail.permission": + "No tens permís per canviar el correu electrònic per a l'usuari \"{name}\"", + "error.user.changeLanguage.permission": + "No tens permís per canviar l'idioma de l'usuari \"{name}\"", + "error.user.changeName.permission": + "No tens permís per canviar el nom de l'usuari \"{name}\"", + "error.user.changePassword.permission": + "No tens permís per canviar la contrasenya de l'usuari \"{name}\"", + "error.user.changeRole.lastAdmin": + "El rol del darrer administrador no es pot canviar", + "error.user.changeRole.permission": + "No tens permís per canviar el rol de l'usuari \"{name}\"", + "error.user.changeRole.toAdmin": + "No tens permís per promocionar algú al rol d’administrador", + "error.user.create.permission": "No tens permís per crear aquest usuari", + "error.user.delete": "L'usuari \"{name}\" no es pot eliminar", + "error.user.delete.lastAdmin": "No es pot eliminar l'\u00faltim administrador", + "error.user.delete.lastUser": "El darrer usuari no es pot eliminar", + "error.user.delete.permission": + "No pots eliminar l'usuari \"{name}\"", + "error.user.duplicate": + "Ja existeix un usuari amb l'adreça electrònica \"{email}\"", + "error.user.email.invalid": "Si us plau, introdueix una adreça de correu electrònic vàlida", + "error.user.language.invalid": "Introduïu un idioma vàlid", + "error.user.notFound": "L'usuari \"{name}\" no s'ha trobat", + "error.user.password.invalid": + "Introduïu una contrasenya vàlida. Les contrasenyes han de tenir com a mínim 8 caràcters.", + "error.user.password.notSame": "Les contrasenyes no coincideixen", + "error.user.password.undefined": "L'usuari no té una contrasenya", + "error.user.role.invalid": "Si us plau, introdueix un rol vàlid", + "error.user.update.permission": + "No tens permís per actualitzar l'usuari \"{name}\"", + + "error.validation.accepted": "Si us plau confirma", + "error.validation.alpha": "Si us plau, introdueix únicament caràcters entre a-z", + "error.validation.alphanum": + "Si us plau, introdueix únicament caràcters entre a-z o números de 0-9", + "error.validation.between": + "Introdueix un valor entre \"{min}\" i \"{max}\"", + "error.validation.boolean": "Si us plau confirma o denega", + "error.validation.contains": + "Si us plau, introduïu un valor que contingui \"{needle}\"", + "error.validation.date": "Si us plau, introdueix una data vàlida", + "error.validation.date.after": "Introdueix una data posterior {date}", + "error.validation.date.before": "Introdueix una data anterior {date}", + "error.validation.date.between": "Introdueix una data entre {min} i {max}", + "error.validation.denied": "Si us plau, denegui", + "error.validation.different": "El valor no ha de ser \"{other}\"", + "error.validation.email": "Si us plau, introdueix una adreça de correu electrònic vàlida", + "error.validation.endswith": "El valor ha de finalitzar amb \"{end}\"", + "error.validation.filename": "Si us plau, introdueix un nom de fitxer vàlid", + "error.validation.in": "Si us plau, introduïu una de les opcions següents: ({in})", + "error.validation.integer": "Si us plau, introduïu un nombre enter vàlid", + "error.validation.ip": "Si us plau, introduïu una adreça IP vàlida", + "error.validation.less": "Si us plau, introduïu un valor inferior a {max}", + "error.validation.match": "El valor no coincideix amb el patró esperat", + "error.validation.max": "Si us plau, introduïu un valor igual o inferior a {max}", + "error.validation.maxlength": + "Si us plau, introduïu un valor més curt. (màxim {max} caràcters)", + "error.validation.maxwords": "Si us plau, introduïu no més de {max} paraula(es)", + "error.validation.min": "Si us plau, introduïu un valor igual o superior a {min}", + "error.validation.minlength": + "Si us plau, introduïu un valor més llarg. (min. {min} caràcters)", + "error.validation.minwords": "Si us plau, introduïu almenys {min} paraula(es)", + "error.validation.more": "Si us plau, introduïu un valor més gran que {min}", + "error.validation.notcontains": + "Introduïu un valor que no contingui \"{needle}\"", + "error.validation.notin": + "Si us plau, no introduïu cap d'aquests elements: ({notIn})", + "error.validation.option": "Si us plau, seleccioneu una opció vàlida", + "error.validation.num": "Si us plau, introduïu un número vàlid", + "error.validation.required": "Si us plau, introduïu alguna cosa", + "error.validation.same": "Si us plau, introduïu \"{other}\"", + "error.validation.size": "La mida del valor ha de ser \"{size}\"", + "error.validation.startswith": "El valor ha de començar amb \"{start}\"", + "error.validation.time": "Si us plau, introduïu una hora vàlida", + "error.validation.url": "Si us plau, introduïu una URL vàlida", + + "field.required": "El camp és obligatori", + "field.files.empty": "Encara no hi ha cap fitxer seleccionat", + "field.pages.empty": "Encara no s'ha seleccionat cap pàgina", + "field.structure.delete.confirm": "Segur que voleu eliminar aquesta fila?", + "field.structure.empty": "Encara no hi ha entrades.", + "field.users.empty": "Encara no s'ha seleccionat cap usuari", + + "file.delete.confirm": + "Esteu segurs d'eliminar
{filename}?", + + "files": "Arxius", + "files.empty": "Encara no hi ha fitxers", + + "hour": "Hora", + "insert": "Insertar", + "install": "Instal·lar", + + "installation": "Instal·lació", + "installation.completed": "S'ha instal·lat el panell", + "installation.disabled": "L'instal·lador del panell està desactivat per defecte als servidors públics. Si us plau, executeu l'instal·lador en una màquina local o habiliteu-lo amb l'opció panel.install", + "installation.issues.accounts": + "La carpeta /site/accounts no existeix o no es pot escriure", + "installation.issues.content": + "La carpeta /content no existeix o no es pot escriure", + "installation.issues.curl": "Es requereix l'extensió CURL", + "installation.issues.headline": "El panell no es pot instal·lar", + "installation.issues.mbstring": + "Es requereix l'extensió de MB String", + "installation.issues.media": + "La carpeta /media no existeix o no es pot escriure", + "installation.issues.php": "Assegureu-vos d'utilitzar PHP 7+", + "installation.issues.server": + "Kirby requereix Apache, Nginx o Caddy", + "installation.issues.sessions": "La carpeta /site/sessions no existeix o no es pot escriure", + + "language": "Idioma", + "language.code": "Codi", + "language.convert": "Fer per defecte", + "language.convert.confirm": + "

Segur que voleu convertir {name} a l'idioma predeterminat? Això no es pot desfer.

Si {name} té contingut no traduït, ja no podreu tornar enrere i algunes parts del vostre lloc poden quedar buides.

", + "language.create": "Afegir un nou idioma", + "language.delete.confirm": + "Segur que voleu eliminar l'idioma {name} incloent totes les traduccions? Això no es pot desfer!", + "language.deleted": "S'ha suprimit l'idioma", + "language.direction": "Direcció de lectura", + "language.direction.ltr": "Esquerra a dreta", + "language.direction.rtl": "De dreta a esquerra", + "language.locale": "Cadena local de PHP", + "language.locale.warning": "S'està fent servir una configuració regional personalitzada. Modifica el fitxer d'idioma a /site/languages", + "language.name": "Nom", + "language.updated": "S'ha actualitzat l'idioma", + + "languages": "Idiomes", + "languages.default": "Idioma per defecte", + "languages.empty": "Encara no hi ha cap idioma", + "languages.secondary": "Idiomes secundaris", + "languages.secondary.empty": "Encara no hi ha idiomes secundaris", + + "license": "Llic\u00e8ncia Kirby", + "license.buy": "Comprar una llicència", + "license.register": "Registrar", + "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", + + "link": "Enlla\u00e7", + "link.text": "Enllaç de text", + + "loading": "Carregant", + + "lock.unsaved": "Canvis no guardats", + "lock.unsaved.empty": "Ja no hi ha canvis no guardats", + "lock.isLocked": "Canvis no guardats per {email}", + "lock.file.isLocked": "El fitxer està sent editat actualment per {email} i no pot ser modificat.", + "lock.page.isLocked": "La pàgina està sent editat actualment per {email} i no pot ser modificat.", + "lock.unlock": "Desbloquejar", + "lock.isUnlocked": "Els teus canvis sense guardar han estat sobreescrits per a un altra usuario. Pots descarregar els teus canvis per combinar-los manualment.", + + "login": "Entrar", + "login.remember": "Manten-me connectat", + + "logout": "Tancar sessi\u00f3", + + "menu": "Menú", + "meridiem": "AM/PM", + "mime": "Tipus de mitjà", + "minutes": "Minuts", + + "month": "Mes", + "months.april": "Abril", + "months.august": "Agost", + "months.december": "Desembre", + "months.february": "Febrer", + "months.january": "Gener", + "months.july": "Juliol", + "months.june": "Juny", + "months.march": "Mar\u00e7", + "months.may": "Maig", + "months.november": "Novembre", + "months.october": "Octubre", + "months.september": "Setembre", + + "more": "Més", + "name": "Nom", + "next": "Següent", + "off": "apagat", + "on": "encès", + "open": "Obrir", + "options": "Opcions", + + "orientation": "Orientació", + "orientation.landscape": "Horitzontal", + "orientation.portrait": "Vertical", + "orientation.square": "Quadrat", + + "page.changeSlug": "Canviar URL", + "page.changeSlug.fromTitle": "Crear a partir del t\u00edtol", + "page.changeStatus": "Canviar l'estat", + "page.changeStatus.position": "Si us plau, seleccioneu una posició", + "page.changeStatus.select": "Seleccioneu un nou estat", + "page.changeTemplate": "Canviar la plantilla", + "page.delete.confirm": + "Segur que voleu eliminar {title}?", + "page.delete.confirm.subpages": + "Aquesta pàgina té subpàgines.
Totes les subpàgines també s'eliminaran.", + "page.delete.confirm.title": "Introduïu el títol de la pàgina per confirmar", + "page.draft.create": "Crear un esborrany", + "page.duplicate.appendix": "Copiar", + "page.duplicate.files": "Copiar fitxers", + "page.duplicate.pages": "Copiar pàgines", + "page.status": "Estat", + "page.status.draft": "Esborrany", + "page.status.draft.description": + "La pàgina està en mode d'esborrany i només és visible per als editors registrats", + "page.status.listed": "Públic", + "page.status.listed.description": "La pàgina és pública per a tothom", + "page.status.unlisted": "Sense classificar", + "page.status.unlisted.description": "La pàgina només es pot accedir a través de l'URL", + + "pages": "Pàgines", + "pages.empty": "Encara no hi ha pàgines", + "pages.status.draft": "Esborranys", + "pages.status.listed": "Publicat", + "pages.status.unlisted": "Sense classificar", + + "pagination.page": "Pàgina", + + "password": "Contrasenya", + "pixel": "Pixel", + "prev": "Anterior", + "remove": "Eliminar", + "rename": "Canviar el nom", + "replace": "Reempla\u00e7ar", + "retry": "Reintentar", + "revert": "Revertir", + + "role": "Rol", + "role.admin.description": "L’administrador té tots els permisos", + "role.admin.title": "Administrador", + "role.all": "Tots", + "role.empty": "No hi ha usuaris amb aquest rol", + "role.description.placeholder": "Sense descripció", + "role.nobody.description": "Aquest és un rol per defecte sense permisos", + "role.nobody.title": "Ningú", + + "save": "Desar", + "search": "Cercar", + + "section.required": "La secció és obligatòria", + + "select": "Seleccionar", + "settings": "Configuració", + "size": "Tamany", + "slug": "URL-ap\u00e8ndix", + "sort": "Ordenar", + "title": "Títol", + "template": "Plantilla", + "today": "Avui", + + "toolbar.button.code": "Codi", + "toolbar.button.bold": "Negreta", + "toolbar.button.email": "Email", + "toolbar.button.headings": "Encapçalaments", + "toolbar.button.heading.1": "Encapçalament 1", + "toolbar.button.heading.2": "Encapçalament 2", + "toolbar.button.heading.3": "Encapçalament 3", + "toolbar.button.italic": "Cursiva", + "toolbar.button.file": "Arxiu", + "toolbar.button.file.select": "Selecciona un fitxer", + "toolbar.button.file.upload": "Carrega un fitxer", + "toolbar.button.link": "Enlla\u00e7", + "toolbar.button.ol": "Llista ordenada", + "toolbar.button.ul": "Llista de vinyetes", + + "translation.author": "Equip Kirby", + "translation.direction": "ltr", + "translation.name": "Catalan", + "translation.locale": "ca_ES", + + "upload": "Carregar", + "upload.error.cantMove": "El fitxer carregat no s'ha pogut moure", + "upload.error.cantWrite": "No s'ha pogut escriure el fitxer al disc", + "upload.error.default": "No s'ha pogut carregar el fitxer", + "upload.error.extension": "La càrrega del fitxer s'ha aturat per l'extensió", + "upload.error.formSize": "El fitxer carregat supera la directiva MAX_FILE_SIZE especificada en el formulari", + "upload.error.iniPostSize": "El fitxer carregat supera la directiva post_max_size especifiada al php.ini", + "upload.error.iniSize": "El fitxer carregat supera la directiva upload_max_filesize especifiada al php.ini", + "upload.error.noFile": "No s'ha carregat cap fitxer", + "upload.error.noFiles": "No s'ha penjat cap fitxer", + "upload.error.partial": "El fitxer carregat només s'ha carregat parcialment", + "upload.error.tmpDir": "Falta una carpeta temporal", + "upload.errors": "Error", + "upload.progress": "Carregant...", + + "url": "Url", + "url.placeholder": "https://example.com", + + "user": "Usuari", + "user.blueprint": + "Podeu definir seccions addicionals i camps de formulari per a aquest rol d'usuari a /site/blueprints/users/{role}.yml", + "user.changeEmail": "Canviar e-mail", + "user.changeLanguage": "Canviar idioma", + "user.changeName": "Canviar el nom d'aquest usuari", + "user.changePassword": "Canviar contrasenya", + "user.changePassword.new": "Nova contrasenya", + "user.changePassword.new.confirm": "Confirma la nova contrasenya ...", + "user.changeRole": "Canviar el rol", + "user.changeRole.select": "Seleccionar un nou rol", + "user.create": "Afegir un nou usuari", + "user.delete": "Eliminar aquest usuari", + "user.delete.confirm": + "Segur que voleu eliminar
{email}?", + + "users": "Usuaris", + + "version": "Versi\u00f3 de Kirby", + + "view.account": "La teva compta", + "view.installation": "Instal·lació", + "view.settings": "Configuració", + "view.site": "Lloc web", + "view.users": "Usuaris", + + "welcome": "Benvinguda", + "year": "Any" +} diff --git a/kirby/i18n/translations/cs.json b/kirby/i18n/translations/cs.json new file mode 100755 index 0000000..0b40e54 --- /dev/null +++ b/kirby/i18n/translations/cs.json @@ -0,0 +1,481 @@ +{ + "add": "P\u0159idat", + "avatar": "Profilov\u00fd obr\u00e1zek", + "back": "Zpět", + "cancel": "Zru\u0161it", + "change": "Zm\u011bnit", + "close": "Zav\u0159it", + "confirm": "Ok", + "copy": "Kopírovat", + "create": "Vytvořit", + + "date": "Datum", + "date.select": "Vyberte datum", + + "day": "Den", + "days.fri": "p\u00e1", + "days.mon": "po", + "days.sat": "so", + "days.sun": "ne", + "days.thu": "\u010dt", + "days.tue": "\u00fat", + "days.wed": "st", + + "delete": "Smazat", + "dimensions": "Rozměry", + "disabled": "Zakázáno", + "discard": "Zahodit", + "download": "Stáhnout", + "duplicate": "Duplikovat", + "edit": "Upravit", + + "dialog.files.empty": "Žádné soubory k výběru", + "dialog.pages.empty": "Žádné stránky k výběru", + "dialog.users.empty": "Žádní uživatelé k výběru", + + "email": "Email", + "email.placeholder": "mail@example.com", + + "error.access.login": "Neplatné přihlášení", + "error.access.panel": "Nemáte povoleno vstoupit do panelu", + "error.access.view": "Nejste oprávněni vstoupit do této části panelu.", + + "error.avatar.create.fail": "Nebylo možné nahrát profilový obrázek", + "error.avatar.delete.fail": "Nebylo mo\u017en\u00e9 smazat profilov\u00fd obr\u00e1zek", + "error.avatar.dimensions.invalid": + "Výšku a šířka profilového obrázku by měla být pod 3000 pixelů", + "error.avatar.mime.forbidden": + "Profilový obrázek musí být ve formátu JPEG nebo PNG", + + "error.blueprint.notFound": "Nelze načíst blueprint \"{name}\" ", + + "error.email.preset.notFound": "Nelze nalézt emailové přednastavení \"{name}\"", + + "error.field.converter.invalid": "Neplatný konvertor \"{converter}\"", + + "error.file.changeName.empty": "Toto jméno nesmí být prázdné", + "error.file.changeName.permission": + "Nemáte povoleno změnit jméno souboru \"{filename}\"", + "error.file.duplicate": "Soubor s názvem \"{filename}\" již existuje", + "error.file.extension.forbidden": + "Přípona souboru \"{extension}\" není povolena", + "error.file.extension.missing": + "Nem\u016f\u017eete nahr\u00e1t soubor bez p\u0159\u00edpony", + "error.file.maxheight": "The height of the image must not exceed {height} pixels", + "error.file.maxsize": "The file is too large", + "error.file.maxwidth": "The width of the image must not exceed {width} pixels", + "error.file.mime.differs": + "Nahraný soubor musí být stejného typu \"{mime}\"", + "error.file.mime.forbidden": "Soubor typu \"{mime}\" není povolený", + "error.file.mime.invalid": "Invalid mime type: {mime}", + "error.file.mime.missing": + "Nelze rozeznat mime typ souboru \"{filename}\"", + "error.file.minheight": "The height of the image must be at least {height} pixels", + "error.file.minsize": "The file is too small", + "error.file.minwidth": "The width of the image must be at least {width} pixels", + "error.file.name.missing": "Název souboru nesmí být prázdný", + "error.file.notFound": "Soubor se nepoda\u0159ilo nal\u00e9zt", + "error.file.orientation": "The orientation of the image must be \"{orientation}\"", + "error.file.type.forbidden": "Nemáte povoleno nahrávat soubory typu {type} ", + "error.file.undefined": "Soubor se nepoda\u0159ilo nal\u00e9zt", + + "error.form.incomplete": "Prosím opravte všechny chyby ve formuláři", + "error.form.notSaved": "Formulář nemohl být uložen", + + "error.language.code": "Zadejte prosím platný kód jazyka", + "error.language.duplicate": "Jazyk již existuje", + "error.language.name": "Zadejte prosím platné jméno jazyka", + + "error.license.format": "Zadejte prosím platné licenční číslo", + "error.license.email": "Zadejte prosím platnou emailovou adresu", + "error.license.verification": "Licenci nelze ověřit", + + "error.page.changeSlug.permission": + "Nem\u016f\u017eete zm\u011bnit URL t\u00e9to str\u00e1nky", + "error.page.changeStatus.incomplete": + "Stránka obsahuje chyby a nemohla být zveřejněna", + "error.page.changeStatus.permission": + "Status této stránky nelze změnit", + "error.page.changeStatus.toDraft.invalid": + "Stránka \"{slug}\" nemůže být převedena na koncept", + "error.page.changeTemplate.invalid": + "Šablonu stránky \"{slug}\" nelze změnit", + "error.page.changeTemplate.permission": + "Nemáte dovoleno změnit šablonu stránky \"{slug}\"", + "error.page.changeTitle.empty": "Titulek nesmí být prázdný", + "error.page.changeTitle.permission": + "Nemáte dovoleno změnit titulek stránky \"{slug}\"", + "error.page.create.permission": "Nemáte dovoleno vytvořit \"{slug}\"", + "error.page.delete": "Stránku \"{slug}\" nelze vymazat", + "error.page.delete.confirm": "Pro potvrzení prosím zadejte titulek stránky", + "error.page.delete.hasChildren": + "Stránka má podstránky, nemůže být vymazána", + "error.page.delete.permission": "Nemáte dovoleno odstranit \"{slug}\"", + "error.page.draft.duplicate": + "Koncept stránky, který obsahuje v adrese URL \"{slug}\" již existuje ", + "error.page.duplicate": + "Stránka, která v adrese URL obsahuje \"{slug}\" již existuje", + "error.page.duplicate.permission": "Nemáte dovoleno duplikovat \"{slug}\"", + "error.page.notFound": "Str\u00e1nku se nepoda\u0159ilo nal\u00e9zt.", + "error.page.num.invalid": + "Zadejte prosím platné pořadové číslo. Čísla nesmí být záporná.", + "error.page.slug.invalid": "Zadejte prosím platnou předponu URL", + "error.page.sort.permission": "Stránce \"{slug}\" nelze změnit pořadí", + "error.page.status.invalid": "Nastavte prosím platný status stránky", + "error.page.undefined": "Str\u00e1nku se nepoda\u0159ilo nal\u00e9zt.", + "error.page.update.permission": "Nemáte dovoleno upravit \"{slug}\"", + + "error.section.files.max.plural": + "Sekce \"{section}\" nesmí obsahovat více jak {max} souborů", + "error.section.files.max.singular": + "Sekce \"{section}\" může obsahovat nejvýše jeden soubor", + "error.section.files.min.plural": + "Sekce \"{section}\" vyžaduje nejméně {min} souborů", + "error.section.files.min.singular": + "Sekce \"{section}\" vyžaduje alespoň jeden soubor", + + "error.section.pages.max.plural": + "Sekce \"{section}\" nesmí obsahovat více jak {max} stránek", + "error.section.pages.max.singular": + "Sekce \"{section}\" může obsahovat nejvýše jednu stránku", + "error.section.pages.min.plural": + "Sekce \"{section}\" vyžaduje alespoň {min} stránek", + "error.section.pages.min.singular": + "Sekce \"{section}\" vyžaduje alespoň jednu stránku", + + "error.section.notLoaded": "Nelze načíst sekci \"{name}\"", + "error.section.type.invalid": "Typ sekce \"{type}\" není platný", + + "error.site.changeTitle.empty": "Titulek nesmí být prázdný", + "error.site.changeTitle.permission": + "Nemáte dovoleno změnit titulek stránky", + "error.site.update.permission": "Nemáte dovoleno upravit stránku", + + "error.template.default.notFound": "Výchozí šablona neexistuje", + + "error.user.changeEmail.permission": + "Nemáte dovoleno měnit email uživatele \"{name}\"", + "error.user.changeLanguage.permission": + "Nemáte dovoleno změnit jazyk uživatele \"{name}\"", + "error.user.changeName.permission": + "Nemáte dovoleno změnit jméno uživatele \"{name}\"", + "error.user.changePassword.permission": + "Nemáte dovoleno změnit heslo uživatele \"{name}\"", + "error.user.changeRole.lastAdmin": + "Role posledního administrátora nemůže být změněna", + "error.user.changeRole.permission": + "Nemáte dovoleno změnit roli uživatele \"{name}\"", + "error.user.changeRole.toAdmin": + "Nemáte dovoleno povýšit uživatele do role administrátora.", + "error.user.create.permission": "Nemáte dovoleno vytvořit tohoto uživatele", + "error.user.delete": "U\u017eivatel nemohl b\u00fdt smaz\u00e1n", + "error.user.delete.lastAdmin": "Nem\u016f\u017eete smazat posledn\u00edho administr\u00e1tora", + "error.user.delete.lastUser": "Poslední uživatel nemůže být smazán", + "error.user.delete.permission": + "Nem\u00e1te dovoleno smazat tohoto u\u017eivatele", + "error.user.duplicate": + "Uživatel s emailovou adresou \"{email}\" již existuje", + "error.user.email.invalid": "Zadejte prosím platnou emailovou adresu", + "error.user.language.invalid": "Zadejte prosím platný jazyk", + "error.user.notFound": "U\u017eivatele se nepoda\u0159ilo nal\u00e9zt", + "error.user.password.invalid": + "Zadejte prosím platné heslo. Heslo musí být dlouhé alespoň 8 znaků.", + "error.user.password.notSame": "Pros\u00edm potvr\u010fte heslo", + "error.user.password.undefined": "Uživatel nemá nastavené heslo.", + "error.user.role.invalid": "Zadejte prosím platnou roli", + "error.user.update.permission": + "Nemáte dovoleno upravit uživatele \"{name}\"", + + "error.validation.accepted": "Potvrďte prosím", + "error.validation.alpha": "Zadávejte prosím pouze znaky v rozmezí a-z", + "error.validation.alphanum": + "Zadávejte prosím pouze znaky v rozmezí a-z nebo čísla v rozmezí 0-9", + "error.validation.between": + "Zadejte prosím hodnotu mez \"{min}\" a \"{max}\"", + "error.validation.boolean": "Potvrďte prosím, nebo odmítněte", + "error.validation.contains": + "Zadejte prosím hodnotu, která obsahuje \"{needle}\"", + "error.validation.date": "Zadejte prosím platné datum", + "error.validation.date.after": "Zadejte prosím datum po {date}", + "error.validation.date.before": "Zadejte prosím datum před {date}", + "error.validation.date.between": "Zadejte prosím datum mezi {min} a {max}", + "error.validation.denied": "Prosím, odmítněte", + "error.validation.different": "Hodnota nesmí být \"{other}\"", + "error.validation.email": "Zadejte prosím platnou emailovou adresu", + "error.validation.endswith": "Hodnota nesmí končit \"{end}\"", + "error.validation.filename": "Zadejte prosím platný název souboru", + "error.validation.in": "Zadejte prosím některou z následujíích hodnot: ({in})", + "error.validation.integer": "Zadejte prosím platné celé číslo", + "error.validation.ip": "Zadejte prosím platnou IP adresu", + "error.validation.less": "Zadejte prosím hodnotu menší než {max}", + "error.validation.match": "Hodnota neodpovídá očekávanému vzoru", + "error.validation.max": "Zadejte prosím hodnotu rovnou, nebo menší než {max}", + "error.validation.maxlength": + "Zadaná hodnota je příliš dlouhá. (Povoleno nejvýše {max} znaků)", + "error.validation.maxwords": "Nezadávejte prosím více jak {max} slov", + "error.validation.min": "Zadejte prosím hodnotu rovnou, nebo větší než {min}", + "error.validation.minlength": + "Zadaná hodnota je příliš krátká. (Požadováno nejméně {min} znaků)", + "error.validation.minwords": "Zadejte prosím alespoň {min} slov", + "error.validation.more": "Zadejte prosím hodnotu větší než {min}", + "error.validation.notcontains": + "Zadejte prosím hodnotu, která neobsahuje \"{needle}\"", + "error.validation.notin": + "Nezadávejte prosím žádnou z následujíích hodnot: ({notIn})", + "error.validation.option": "Vyberte prosím platnou možnost", + "error.validation.num": "Zadejte prosím platné číslo", + "error.validation.required": "Zadejte prosím jakoukoli hodnotu", + "error.validation.same": "Zadejte prosím \"{other}\"", + "error.validation.size": "Velikost hodnoty musí být \"{size}\"", + "error.validation.startswith": "Hodnota musí začínat \"{start}\"", + "error.validation.time": "Zadejte prosím platný čas", + "error.validation.url": "Zadejte prosím platnou adresu URL", + + "field.required": "Pole musí být vyplněno.", + "field.files.empty": "Nebyly zatím vybrány žádné soubory", + "field.pages.empty": "Nebyly zatím vybrány žádné stránky", + "field.structure.delete.confirm": "Opravdu chcete smazat tento z\u00e1znam?", + "field.structure.empty": "Zat\u00edm nejsou \u017e\u00e1dn\u00e9 z\u00e1znamy.", + "field.users.empty": "Nebyli zatím vybráni žádní uživatelé", + + "file.delete.confirm": + "Opravdu chcete smazat tento soubor?", + + "files": "Soubory", + "files.empty": "Zatím žádné soubory", + + "hour": "Hodina", + "insert": "Vlo\u017eit", + "install": "Instalovat", + + "installation": "Instalace", + "installation.completed": "Panel byl nainstalován", + "installation.disabled": "Instalátor panelu je ve výchozím nastavení na veřejných serverech zakázán. Spusťte prosím instalátor na lokálním počítači nebo jej povolte prostřednictvím panel.install.", + "installation.issues.accounts": + "\/site\/accounts nen\u00ed zapisovateln\u00e9", + "installation.issues.content": + "Slo\u017eka content a v\u0161echny soubory a slo\u017eky v n\u00ed mus\u00ed b\u00fdt zapisovateln\u00e9.", + "installation.issues.curl": "Je vyžadováno rozšířeníCURL", + "installation.issues.headline": "Panel nelze nainstalovat", + "installation.issues.mbstring": + "Je vyžadováno rozšířeníMB String", + "installation.issues.media": + "Složka/media neexistuje, nebo nemá povolený zápis", + "installation.issues.php": "Ujistěte se, že používátePHP 7+", + "installation.issues.server": + "Kirby vyžadujeApache, Nginx neboCaddy", + "installation.issues.sessions": "Složka/site/sessions neexistuje, nebo nemá povolený zápis", + + "language": "Jazyk", + "language.code": "Kód", + "language.convert": "Nastavte výchozí možnost", + "language.convert.confirm": + "

Opravdu chcete převést{name} na výchozí jazyk? Tuto volbu nelze vzít zpátky.

Pokud {name} obsahuje nepřeložený text, nebude již k dispozici záložní varianta a části stránky mohou zůstat prázdné.

", + "language.create": "Přidat nový jazyk", + "language.delete.confirm": + "Opravdu chcete smazat jazyk {name} včetně všech překladů? Tuto volbu nelze vzít zpátky!", + "language.deleted": "Jazyk byl smazán", + "language.direction": "Směr čtení", + "language.direction.ltr": "Zleva doprava", + "language.direction.rtl": "Zprava doleva", + "language.locale": "Řetězec lokalizace PHP", + "language.locale.warning": "Používáte vlastní jazykové nastavení. Upravte prosím soubor s nastavením v /site/languages", + "language.name": "Jméno", + "language.updated": "Jazyk byl aktualizován", + + "languages": "Jazyky", + "languages.default": "Výchozí jazyk", + "languages.empty": "Zatím neexistují žádné jazyky", + "languages.secondary": "Další jazyky", + "languages.secondary.empty": "Neexistují zatím žádné další jazyky", + + "license": "Kirby licence", + "license.buy": "Zakoupit licenci", + "license.register": "Registrovat", + "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", + + "link": "Odkaz", + "link.text": "Text odkazu", + + "loading": "Načítám", + + "lock.unsaved": "Neuložené změny", + "lock.unsaved.empty": "Nezbývají již žádné neuložené změny.", + "lock.isLocked": "Neuložené změny provedené {email}", + "lock.file.isLocked": "Soubor nelze změnit, právě jej upravuje {email}.", + "lock.page.isLocked": "Stránku nelze změnit, právě jí upravuje {email} .", + "lock.unlock": "Odemknout", + "lock.isUnlocked": "Vaše neuložené změny byly přepsány jiným uživatelem. Můžeze si své úpravy stáhnout a zapracovat je ručně.", + + "login": "P\u0159ihl\u00e1sit se", + "login.remember": "Zůstat přihlášen", + + "logout": "Odhl\u00e1sit se", + + "menu": "Menu", + "meridiem": "AM/PM", + "mime": "Typ média", + "minutes": "Minuty", + + "month": "Měsíc", + "months.april": "Duben", + "months.august": "Srpen", + "months.december": "Prosinec", + "months.february": "\u00danor", + "months.january": "Leden", + "months.july": "\u010cervenec", + "months.june": "\u010cerven", + "months.march": "B\u0159ezen", + "months.may": "Kv\u011bten", + "months.november": "Listopad", + "months.october": "\u0158\u00edjen", + "months.september": "Z\u00e1\u0159\u00ed", + + "more": "Více", + "name": "Jméno", + "next": "Další", + "off": "vypnuto", + "on": "zapnuto", + "open": "Otevřít", + "options": "Možnosti", + + "orientation": "Orientace", + "orientation.landscape": "Na šířku", + "orientation.portrait": "Na výšku", + "orientation.square": "Čtverec", + + "page.changeSlug": "Zm\u011bnit URL", + "page.changeSlug.fromTitle": "Vytvo\u0159it z n\u00e1zvu", + "page.changeStatus": "Změnit status", + "page.changeStatus.position": "Vyberte prosím pozici", + "page.changeStatus.select": "Vybrat nový status", + "page.changeTemplate": "Změnit šablonu", + "page.delete.confirm": + "Opravdu chcete smazat tuto str\u00e1nku?", + "page.delete.confirm.subpages": + "Tato stránka má podstránky.
Všechny podstránky budou vymazány.", + "page.delete.confirm.title": "Pro potvrzení zadejte titulek stránky", + "page.draft.create": "Vytvořit koncept", + "page.duplicate.appendix": "Kopírovat", + "page.duplicate.files": "Kopírovat soubory", + "page.duplicate.pages": "Kopírovat stránky", + "page.status": "Stav", + "page.status.draft": "Koncept", + "page.status.draft.description": + "Stránka je ve stavu konceptu a je viditelná pouze pro přihlášené editory", + "page.status.listed": "Veřejná", + "page.status.listed.description": "Stránka je zveřejněná pro všechny", + "page.status.unlisted": "Neveřejná", + "page.status.unlisted.description": "Tato stránka je dostupná pouze přes URL.", + + "pages": "Stránky", + "pages.empty": "Zatím žádné stránky", + "pages.status.draft": "Koncepty", + "pages.status.listed": "Zveřejněno", + "pages.status.unlisted": "Neveřejná", + + "pagination.page": "Stránka", + + "password": "Heslo", + "pixel": "Pixel", + "prev": "Předchozí", + "remove": "Odstranit", + "rename": "Přejmenovat", + "replace": "Nahradit", + "retry": "Zkusit znovu", + "revert": "Zahodit", + + "role": "Role", + "role.admin.description": "Administrátor má všechna práva", + "role.admin.title": "Administrátor", + "role.all": "Vše", + "role.empty": "Neexistují uživatelé s touto rolí", + "role.description.placeholder": "Žádný popis", + "role.nobody.description": "Toto je výchozí role bez jakýchkoli oprávnění", + "role.nobody.title": "Nikdo", + + "save": "Ulo\u017eit", + "search": "Hledat", + + "section.required": "Sekce musí být vyplněna", + + "select": "Vybrat", + "settings": "Nastavení", + "size": "Velikost", + "slug": "P\u0159\u00edpona URL", + "sort": "Řadit", + "title": "Název", + "template": "\u0160ablona", + "today": "Dnes", + + "toolbar.button.code": "Kód", + "toolbar.button.bold": "Tu\u010dn\u00fd text", + "toolbar.button.email": "Email", + "toolbar.button.headings": "Nadpisy", + "toolbar.button.heading.1": "Nadpis 1", + "toolbar.button.heading.2": "Nadpis 2", + "toolbar.button.heading.3": "Nadpis 3", + "toolbar.button.italic": "Kurz\u00edva", + "toolbar.button.file": "Soubor", + "toolbar.button.file.select": "Vyberte soubor", + "toolbar.button.file.upload": "Nahrajte soubor", + "toolbar.button.link": "Odkaz", + "toolbar.button.ol": "Řazený seznam", + "toolbar.button.ul": "Odrážkový seznam", + + "translation.author": "Kirby tým", + "translation.direction": "ltr", + "translation.name": "\u010cesky", + "translation.locale": "cs_CZ", + + "upload": "Nahrát", + "upload.error.cantMove": "Nahraný soubor nemohl být přesunut", + "upload.error.cantWrite": "Zápis souboru na disk se nezdařil", + "upload.error.default": "Soubor se nepodařilo nahrát", + "upload.error.extension": "Nahrávání souboru přerušeno rozšířením.", + "upload.error.formSize": "Velikost nahrávaného souboru převyšuje omezení stanovené direktivou MAX_FILE_SIZE", + "upload.error.iniPostSize": "Velikost nahrávaného souboru převyšuje omezení stanovené direktivou post_max_size, která je nastavena v php.ini", + "upload.error.iniSize": "Velikost nahrávaného souboru převyšuje omezení stanovené direktivou upload_max_filesize, která je nastavena v php.ini ", + "upload.error.noFile": "Nebyl nahrán žádný soubor", + "upload.error.noFiles": "Nebyly nahrány žádné soubory", + "upload.error.partial": "Soubor byl nahrán pouze z části", + "upload.error.tmpDir": "Chybí dočasná složka", + "upload.errors": "Chyba", + "upload.progress": "Nahrávání...", + + "url": "Url", + "url.placeholder": "https://example.com", + + "user": "Uživatel", + "user.blueprint": + "Pro tuto uživatelskou roli můžete definovat další sekce a pole v /site/blueprints/users/{role}.yml", + "user.changeEmail": "Změnit email", + "user.changeLanguage": "Změnit jazyk", + "user.changeName": "Přejmenovat tohoto uživatele", + "user.changePassword": "Změnit heslo", + "user.changePassword.new": "Nové heslo", + "user.changePassword.new.confirm": "Potvrdit nové heslo...", + "user.changeRole": "Změnit roli", + "user.changeRole.select": "Vybrat novou roli", + "user.create": "Přidat nového uživatele", + "user.delete": "Smazat tohoto uživatele", + "user.delete.confirm": + "Opravdu chcete smazat tohoto u\u017eivatele?", + + "users": "Uživatelé", + + "version": "Verze Kirby", + + "view.account": "V\u00e1\u0161 \u00fa\u010det", + "view.installation": "Instalace", + "view.settings": "Nastavení", + "view.site": "Stránka", + "view.users": "U\u017eivatel\u00e9", + + "welcome": "Vítejte", + "year": "Rok" +} diff --git a/kirby/i18n/translations/da.json b/kirby/i18n/translations/da.json new file mode 100755 index 0000000..719cc6d --- /dev/null +++ b/kirby/i18n/translations/da.json @@ -0,0 +1,481 @@ +{ + "add": "Ny", + "avatar": "Profilbillede", + "back": "Tilbage", + "cancel": "Annuller", + "change": "\u00c6ndre", + "close": "Luk", + "confirm": "Gem", + "copy": "Kopier", + "create": "Opret", + + "date": "Dato", + "date.select": "Vælg en dato", + + "day": "Dag", + "days.fri": "Fre", + "days.mon": "Man", + "days.sat": "L\u00f8r", + "days.sun": "S\u00f8n", + "days.thu": "Tor", + "days.tue": "Tir", + "days.wed": "Ons", + + "delete": "Slet", + "dimensions": "Dimensioner", + "disabled": "Deaktiveret", + "discard": "Kass\u00e9r", + "download": "Download", + "duplicate": "Dupliker", + "edit": "Rediger", + + "dialog.files.empty": "Ingen filer kan vælges", + "dialog.pages.empty": "Ingen sider kan vælges", + "dialog.users.empty": "Ingen brugere kan vælges", + + "email": "Email", + "email.placeholder": "mail@eksempel.dk", + + "error.access.login": "Ugyldigt log ind", + "error.access.panel": "Du har ikke adgang til panelet", + "error.access.view": "Du har ikke adgang til denne del af panelet", + + "error.avatar.create.fail": "Profilbilledet kunne blev ikke uploadet ", + "error.avatar.delete.fail": "Profilbilledet kunne ikke slettes", + "error.avatar.dimensions.invalid": + "Hold venligst bredte og højde på billedet under 3000 pixels", + "error.avatar.mime.forbidden": + "Uacceptabel fil-type", + + "error.blueprint.notFound": "Blueprint \"{name}\" kunne ikke indlæses", + + "error.email.preset.notFound": "Email preset \"{name}\" findes ikke", + + "error.field.converter.invalid": "Ugyldig converter \"{converter}\"", + + "error.file.changeName.empty": "Navn kan ikke efterlades tomt", + "error.file.changeName.permission": + "Du har ikke tilladelse til at ændre navnet på filen \"{filename}\"", + "error.file.duplicate": "En fil med navnet \"{filename}\" eksisterer allerede", + "error.file.extension.forbidden": + "Uacceptabel fil-endelse", + "error.file.extension.missing": + "Du kan ikke uploade filer uden fil-endelse", + "error.file.maxheight": "Højden på billedet af billedet må ikke være større end {height} pixels", + "error.file.maxsize": "Filen er for stor", + "error.file.maxwidth": "Bredden af billedet må ikke være større end {width} pixels", + "error.file.mime.differs": + "Den uploadede fil skal være af samme mime type \"{mime}\"", + "error.file.mime.forbidden": "Media typen \"{mime}\" er ikke tilladt", + "error.file.mime.invalid": "Ugyldig mime type: {mime}", + "error.file.mime.missing": + "Media typen for \"{filename}\" kan ikke bestemmes", + "error.file.minheight": "Højden af billedet skal mindst være {height} pixels", + "error.file.minsize": "Filen er for lille", + "error.file.minwidth": "Bredden af billedet skal mindst være {width} pixels", + "error.file.name.missing": "Filnavn må ikke være tomt", + "error.file.notFound": "Filen kunne ikke findes", + "error.file.orientation": "Formatet på billedet skal være \"{orientation}\"", + "error.file.type.forbidden": "Du har ikke tilladelse til at uploade {type} filer", + "error.file.undefined": "Filen kunne ikke findes", + + "error.form.incomplete": "Ret venligst alle fejl i formularen...", + "error.form.notSaved": "Formularen kunne ikke gemmes", + + "error.language.code": "Indtast venligst en gyldig kode for sproget", + "error.language.duplicate": "Sproget eksisterer allerede", + "error.language.name": "Indtast venligst et gyldigt navn for sproget", + + "error.license.format": "Indtast venligst en gyldig licensnøgle", + "error.license.email": "Indtast venligst en gyldig email adresse", + "error.license.verification": "Licensen kunne ikke verificeres", + + "error.page.changeSlug.permission": + "Du kan ikke \u00e6ndre denne sides URL", + "error.page.changeStatus.incomplete": + "Siden indeholder fejl og kan derfor ikke udgives", + "error.page.changeStatus.permission": + "Status for denne side kan ikke ændres", + "error.page.changeStatus.toDraft.invalid": + "Siden \"{slug}\" kan ikke konverteres om til en kladde", + "error.page.changeTemplate.invalid": + "Skabelonen for siden \"{slug}\" kan ikke ændres", + "error.page.changeTemplate.permission": + "Du har ikke tilladelse til at ændre skabelonen for \"{slug}\"", + "error.page.changeTitle.empty": "Titlen kan ikke være tom", + "error.page.changeTitle.permission": + "Du har ikke tilladelse til at ændre titlen for \"{slug}\"", + "error.page.create.permission": "Du har ikke tilladelse til at oprette \"{slug}\"", + "error.page.delete": "Siden \"{slug}\" kan ikke slettes", + "error.page.delete.confirm": "Indtast venligst sidens titel for at bekræfte", + "error.page.delete.hasChildren": + "Siden har unsersider og kan derfor ikke slettes", + "error.page.delete.permission": "Du har ikke tilladelse til at slette \"{slug}\"", + "error.page.draft.duplicate": + "En sidekladde med URL-endelsen \"{slug}\" eksisterer allerede", + "error.page.duplicate": + "En side med URL-endelsen \"{slug}\" eksisterer allerede", + "error.page.duplicate.permission": "Du har ikke mulighed for at duplikere \"{slug}\"", + "error.page.notFound": "Siden kunne ikke findes", + "error.page.num.invalid": + "Indtast venligst et gyldigt sorteringsnummer. Nummeret kan ikke være negativt.", + "error.page.slug.invalid": "Indtast venligst en gyldig URL prefix", + "error.page.sort.permission": "Siden \"{slug}\" kan ikke sorteres", + "error.page.status.invalid": "Sæt venligst en gyldig status for siden", + "error.page.undefined": "Siden kunne ikke findes", + "error.page.update.permission": "Du har ikke tilladelse til at opdatere \"{slug}\"", + + "error.section.files.max.plural": + "Du kan ikk tilføje mere end {max} filer til \"{section}\" sektionen", + "error.section.files.max.singular": + "Du kan ikke tilføje mere end en fil til \"{section}\" sektionen", + "error.section.files.min.plural": + "Sektionen \"{section}\" kræver mindst {min} filer", + "error.section.files.min.singular": + "Sektionen \"{section}\" kræver mindst en fil", + + "error.section.pages.max.plural": + "Du kan ikke tilføje flere end {max} sider til \"{section}\" sektionen", + "error.section.pages.max.singular": + "Du kan ikke tilføje mere end een side til \"{section}\" sektionen", + "error.section.pages.min.plural": + "Sektionen \"{section}\" kræver mindst {min} sider", + "error.section.pages.min.singular": + "Sektionen \"{section}\" kræver mindst en side", + + "error.section.notLoaded": "Sektionen \"{section}\" kunne ikke indlæses", + "error.section.type.invalid": "Sektionstypen \"{type}\" er ikke gyldig", + + "error.site.changeTitle.empty": "Titlen kan ikke være tom", + "error.site.changeTitle.permission": + "Du har ikke tilladelse til at ændre titlen på sitet", + "error.site.update.permission": "Du har ikke tilladelse til at opdatere sitet", + + "error.template.default.notFound": "Standardskabelonen eksisterer ikke", + + "error.user.changeEmail.permission": + "Du har ikke tilladelse til at ændre emailen for brugeren \"{name}\"", + "error.user.changeLanguage.permission": + "Du har ikke tilladelse til at ændre sproget for brugeren \"{name}\"", + "error.user.changeName.permission": + "Du har ikke tilladelse til at ændre navn på brugeren \"{name}\"", + "error.user.changePassword.permission": + "Du har ikke tilladelse til at ændre adgangskoden for brugeren \"{name}\"", + "error.user.changeRole.lastAdmin": + "Rollen for den sidste admin kan ikke ændres", + "error.user.changeRole.permission": + "Du har ikke tilladelse til at ændre rollen for brugeren \"{name}\"", + "error.user.changeRole.toAdmin": + "Du har ikke tilladelse til at tildele nogen admin rollen", + "error.user.create.permission": "Du har ikke tilladelse til at oprette denne bruger", + "error.user.delete": "Brugeren kunne ikke slettes", + "error.user.delete.lastAdmin": "Du kan ikke slette den sidste admin", + "error.user.delete.lastUser": "Den sidste bruger kan ikke slettes", + "error.user.delete.permission": + "Du har ikke tilladelse til at slette denne bruger", + "error.user.duplicate": + "En bruger med email adresse \"{email}\" eksisterer allerede", + "error.user.email.invalid": "Indtast venligst en gyldig email adresse", + "error.user.language.invalid": "Indtast venligst et gyldigt sprog", + "error.user.notFound": "Brugeren kunne ikke findes", + "error.user.password.invalid": + "Indtast venligst en gyldig adgangskode. Adgangskoder skal minimum være 8 tegn lange.", + "error.user.password.notSame": "Bekr\u00e6ft venligst adgangskoden", + "error.user.password.undefined": "Brugeren har ikke en adgangskode", + "error.user.role.invalid": "Indtast venligst en gyldig rolle", + "error.user.update.permission": + "Du har ikke tilladelse til at opdatere brugeren \"{name}\"", + + "error.validation.accepted": "Bekræft venligst", + "error.validation.alpha": "Indtast venligst kun bogstaver imellem a-z", + "error.validation.alphanum": + "Indtast venligst kun bogstaver og tal imellem a-z eller 0-9", + "error.validation.between": + "Indtast venligst en værdi imellem \"{min}\" og \"{max}\"", + "error.validation.boolean": "Venligst bekræft eller afvis", + "error.validation.contains": + "Indtast venligst en værdi der indeholder \"{needle}\"", + "error.validation.date": "Indtast venligst en gyldig dato", + "error.validation.date.after": "Indtast venligst en dato efter {date}", + "error.validation.date.before": "Indtast venligst en dato før {date}", + "error.validation.date.between": "Indtast venligst en dato imellem {min} og {max}", + "error.validation.denied": "Venligst afvis", + "error.validation.different": "Værdien må ikke være \"{other}\"", + "error.validation.email": "Indtast venligst en gyldig email adresse", + "error.validation.endswith": "Værdi skal ende med \"{end}\"", + "error.validation.filename": "Indtast venligst et gyldigt filnavn", + "error.validation.in": "Indtast venligst en af følgende: ({in})", + "error.validation.integer": "Indtast et gyldigt tal", + "error.validation.ip": "Indtast en gyldig IP adresse", + "error.validation.less": "Indtast venligst en værdi mindre end {max}", + "error.validation.match": "Værdien matcher ikke det forventede mønster", + "error.validation.max": "Indtast venligst en værdi lig med eller lavere end {max}", + "error.validation.maxlength": + "Indtast venligst en kortere værdi. (maks. {max} karakterer)", + "error.validation.maxwords": "Indtast ikke flere end {max} ord", + "error.validation.min": "Indtast en værdi lig med eller højere end {min}", + "error.validation.minlength": + "Indtast venligst en længere værdi. (min. {min} karakterer)", + "error.validation.minwords": "Indtast venligst mindst {min} ord", + "error.validation.more": "Indtast venligst en værdi større end {min}", + "error.validation.notcontains": + "Indtast venligst en værdi der ikke indeholder \"{needle}\"", + "error.validation.notin": + "Indtast venligst ikke nogen af følgende: ({notIn})", + "error.validation.option": "Vælg venligst en gyldig mulighed", + "error.validation.num": "Indtast venligst et gyldigt nummer", + "error.validation.required": "Indtast venligst noget", + "error.validation.same": "Indtast venligst \"{other}\"", + "error.validation.size": "Størrelsen på værdien skal være \"{size}\"", + "error.validation.startswith": "Værdien skal starte med \"{start}\"", + "error.validation.time": "Indtast venligst et gyldigt tidspunkt", + "error.validation.url": "Indtast venligst en gyldig URL", + + "field.required": "Feltet er påkrævet", + "field.files.empty": "Ingen filer valgt endnu", + "field.pages.empty": "Ingen sider valgt endnu", + "field.structure.delete.confirm": "\u00d8nsker du virkelig at slette denne indtastning?", + "field.structure.empty": "Ingen indtastninger endnu.", + "field.users.empty": "Ingen brugere er valgt", + + "file.delete.confirm": + "\u00d8nsker du virkelig at slette denne fil?", + + "files": "Filer", + "files.empty": "Ingen filer endnu", + + "hour": "Time", + "insert": "Inds\u00e6t", + "install": "Installer", + + "installation": "Installation", + "installation.completed": "Panelet er blevet installeret", + "installation.disabled": "Panel installationen er deaktiveret på offentlige servere som standard. Kør venligst installationen på en lokal maskine eller aktiver det med panel.install panel.install muligheden.", + "installation.issues.accounts": + "\/site\/accounts er ikke skrivbar", + "installation.issues.content": + "Content mappen samt alle underliggende filer og mapper skal v\u00e6re skrivbare.", + "installation.issues.curl": "CURL extension er påkrævet", + "installation.issues.headline": "Panelet kan ikke installeres", + "installation.issues.mbstring": + "MB String extension er påkrævet", + "installation.issues.media": + "/media mappen eksisterer ikke eller er ikke skrivbar", + "installation.issues.php": "Sikre dig at der benyttes PHP 7+", + "installation.issues.server": + "Kirby kræver Apache, Nginx eller Caddy", + "installation.issues.sessions": "/site/sessions mappen eksisterer ikke eller er ikke skrivbar", + + "language": "Sprog", + "language.code": "Kode", + "language.convert": "Gør standard", + "language.convert.confirm": + "

Ønsker du virkelig at konvertere {name} til standardsproget? Dette kan ikke fortrydes.

Hvis {name} har uoversat indhold, vil der ikke længere være et gyldigt tilbagefald og dele af dit website vil måske fremstå tomt.

", + "language.create": "Tilføj nyt sprog", + "language.delete.confirm": + "Ønsker du virkelig at slette sproget {name} inklusiv alle oversættelser? Kan ikke fortrydes!", + "language.deleted": "Sproget er blevet slettet", + "language.direction": "Læseretning", + "language.direction.ltr": "Venstre mod højre", + "language.direction.rtl": "Højre mod venstre", + "language.locale": "PHP locale string", + "language.locale.warning": "Du benytter en brugerdefineret sprogopsætning. Rediger venligst dette i sprogfilen i /site/languages", + "language.name": "Navn", + "language.updated": "Sproget er blevet opdateret", + + "languages": "Sprog", + "languages.default": "Standardsprog", + "languages.empty": "Der er ingen sprog endnu", + "languages.secondary": "Sekundære sprog", + "languages.secondary.empty": "Der er ingen sekundære sprog endnu", + + "license": "Kirby licens", + "license.buy": "Køb en licens", + "license.register": "Registrer", + "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", + + "link": "Link", + "link.text": "Link tekst", + + "loading": "Indlæser", + + "lock.unsaved": "Ugemte ændringer", + "lock.unsaved.empty": "Der er ikke flere ændringer der ikke er gamt", + "lock.isLocked": "Ugemte ændringer af {email}", + "lock.file.isLocked": "Filen redigeres på nuværende af {email} og kan derfor ikke ændres.", + "lock.page.isLocked": "Siden redigeres på nuværende af {email} og kan derfor ikke ændres.", + "lock.unlock": "Lås op", + "lock.isUnlocked": "Dine ugemte ændringer er blevet overskrevet af en anden bruger. Du kan downloade dine ændringer for at flette dem ind manuelt.", + + "login": "Log ind", + "login.remember": "Forbliv logget ind", + + "logout": "Log ud", + + "menu": "Menu", + "meridiem": "AM/PM", + "mime": "Medie Type", + "minutes": "Minutter", + + "month": "Måned", + "months.april": "April", + "months.august": "August", + "months.december": "December", + "months.february": "Februar", + "months.january": "Januar", + "months.july": "Juli", + "months.june": "Juni", + "months.march": "Marts", + "months.may": "Maj", + "months.november": "November", + "months.october": "Oktober", + "months.september": "September", + + "more": "Mere", + "name": "Navn", + "next": "Næste", + "off": "Sluk", + "on": "Tænd", + "open": "Åben", + "options": "Indstillinger", + + "orientation": "Orientering", + "orientation.landscape": "Landskab", + "orientation.portrait": "Portræt", + "orientation.square": "Kvadrat", + + "page.changeSlug": "\u00c6ndre URL", + "page.changeSlug.fromTitle": "Generer udfra titel", + "page.changeStatus": "Skift status", + "page.changeStatus.position": "Vælg venligst position", + "page.changeStatus.select": "Vælg en ny status", + "page.changeTemplate": "Skift skabelon", + "page.delete.confirm": + "\u00d8nsker du virkelig at slette denne side?", + "page.delete.confirm.subpages": + "Denne side har undersider.
Alle undersider vil også blive slettet.", + "page.delete.confirm.title": "Indtast sidens titel for at bekræfte", + "page.draft.create": "Opret kladde", + "page.duplicate.appendix": "Kopier", + "page.duplicate.files": "Kopier filer", + "page.duplicate.pages": "Kopier sider", + "page.status": "Status", + "page.status.draft": "Kladde", + "page.status.draft.description": + "Siden er i kladdetilstand og kun synlig for redaktører der er logget ind", + "page.status.listed": "Offentlig", + "page.status.listed.description": "Siden er offentlig for enhver", + "page.status.unlisted": "Ulistede", + "page.status.unlisted.description": "Siden er kun tilgængelig via URL", + + "pages": "Sider", + "pages.empty": "Ingen sider endnu", + "pages.status.draft": "Kladder", + "pages.status.listed": "Udgivede", + "pages.status.unlisted": "Ulistede", + + "pagination.page": "Side", + + "password": "Adgangskode", + "pixel": "Pixel", + "prev": "Forrige", + "remove": "Fjern", + "rename": "Omdøb", + "replace": "Erstat", + "retry": "Pr\u00f8v igen", + "revert": "Kass\u00e9r", + + "role": "Rolle", + "role.admin.description": "Admin har alle rettigheder", + "role.admin.title": "Admin", + "role.all": "All", + "role.empty": "Der er ingen bruger med denne rolle", + "role.description.placeholder": "Ingen beskrivelse", + "role.nobody.description": "Dette er en tilbagefaldsrolle uden rettigheder", + "role.nobody.title": "Ingen", + + "save": "Gem", + "search": "Søg", + + "section.required": "Sektionen er påkrævet", + + "select": "Vælg", + "settings": "Indstillinger", + "size": "Størrelse", + "slug": "URL-appendiks", + "sort": "Sorter", + "title": "Titel", + "template": "Skabelon", + "today": "Idag", + + "toolbar.button.code": "Kode", + "toolbar.button.bold": "Fed tekst", + "toolbar.button.email": "Email", + "toolbar.button.headings": "Overskrifter", + "toolbar.button.heading.1": "Overskrift 1", + "toolbar.button.heading.2": "Overskrift 2", + "toolbar.button.heading.3": "Overskrift 3", + "toolbar.button.italic": "Kursiv tekst", + "toolbar.button.file": "Fil", + "toolbar.button.file.select": "Vælg en fil", + "toolbar.button.file.upload": "Upload en fil", + "toolbar.button.link": "Link", + "toolbar.button.ol": "Ordnet liste", + "toolbar.button.ul": "Punktliste", + + "translation.author": "Kirby Team", + "translation.direction": "ltr", + "translation.name": "Dansk", + "translation.locale": "da_DK", + + "upload": "Upload", + "upload.error.cantMove": "Den uploadede fil kunne ikke flyttes", + "upload.error.cantWrite": "Kunne ikke skrive fil til disk", + "upload.error.default": "Filen kunne ikke uploades", + "upload.error.extension": "Upload af filen blev stoppet af dens type", + "upload.error.formSize": "Filen overskrider MAX_FILE_SIZE direktivet der er specificeret for formularen", + "upload.error.iniPostSize": "FIlen overskrider post_max_size direktivet i php.ini", + "upload.error.iniSize": "FIlen overskrider upload_max_filesize direktivet i php.ini", + "upload.error.noFile": "Ingen fil blev uploadet", + "upload.error.noFiles": "Ingen filer blev uploadet", + "upload.error.partial": "Den uploadede fil blev kun delvist uploadet", + "upload.error.tmpDir": "Der mangler en midlertidig mappe", + "upload.errors": "Fejl", + "upload.progress": "Uploader...", + + "url": "Url", + "url.placeholder": "https://example.com", + + "user": "Bruger", + "user.blueprint": + "Du kan definere yderligere sektioner og formular felter for denne brugerrolle i /site/blueprints/users/{role}.yml", + "user.changeEmail": "Skift email", + "user.changeLanguage": "Skift sprog", + "user.changeName": "Omdøb denne bruger", + "user.changePassword": "Skift adgangskode", + "user.changePassword.new": "Ny adgangskode", + "user.changePassword.new.confirm": "Bekræft den nye adgangskode...", + "user.changeRole": "Skift rolle", + "user.changeRole.select": "Vælg en ny rolle", + "user.create": "Tilføj en ny bruger", + "user.delete": "Slet denne bruger", + "user.delete.confirm": + "\u00d8nsker du virkelig at slette denne bruger?", + + "users": "Brugere", + + "version": "Kirby version", + + "view.account": "Din konto", + "view.installation": "Installation", + "view.settings": "Indstillinger", + "view.site": "Website", + "view.users": "Brugere", + + "welcome": "Velkommen", + "year": "År" +} diff --git a/kirby/i18n/translations/de.json b/kirby/i18n/translations/de.json new file mode 100755 index 0000000..a2e3b3c --- /dev/null +++ b/kirby/i18n/translations/de.json @@ -0,0 +1,481 @@ +{ + "add": "Hinzuf\u00fcgen", + "avatar": "Profilbild", + "back": "Zurück", + "cancel": "Abbrechen", + "change": "\u00c4ndern", + "close": "Schlie\u00dfen", + "confirm": "OK", + "copy": "Kopieren", + "create": "Erstellen", + + "date": "Datum", + "date.select": "Datum auswählen", + + "day": "Tag", + "days.fri": "Fr", + "days.mon": "Mo", + "days.sat": "Sa", + "days.sun": "So", + "days.thu": "Do", + "days.tue": "Di", + "days.wed": "Mi", + + "delete": "L\u00f6schen", + "dimensions": "Maße", + "disabled": "Gesperrt", + "discard": "Verwerfen", + "download": "Download", + "duplicate": "Duplizieren", + "edit": "Bearbeiten", + + "dialog.files.empty": "Keine verfügbaren Dateien", + "dialog.pages.empty": "Keine verfügbaren Seiten", + "dialog.users.empty": "Keine verfügbaren Benutzer", + + "email": "E-Mail", + "email.placeholder": "mail@beispiel.de", + + "error.access.login": "Ungültige Zugangsdaten", + "error.access.panel": "Du hast keinen Zugang zum Panel", + "error.access.view": "Du hast keinen Zugriff auf diesen Teil des Panels", + + "error.avatar.create.fail": "Das Profilbild konnte nicht hochgeladen werden", + "error.avatar.delete.fail": "Das Profilbild konnte nicht gel\u00f6scht werden", + "error.avatar.dimensions.invalid": + "Bitte lade ein Profilbild hoch, das nicht breiter oder höher als 3000 Pixel ist.", + "error.avatar.mime.forbidden": + "Das Profilbild muss vom Format JPEG oder PNG sein", + + "error.blueprint.notFound": "Das Blueprint \"{name}\" konnte nicht geladen werden.", + + "error.email.preset.notFound": "Die E-Mailvorlage \"{name}\" wurde nicht gefunden", + + "error.field.converter.invalid": "Ungültiger Konverter: \"{converter}\"", + + "error.file.changeName.empty": "Bitte gib einen Namen an", + "error.file.changeName.permission": + "Du darfst den Dateinamen von \"{filename}\" nicht ändern", + "error.file.duplicate": "Eine Datei mit dem Dateinamen \"{filename}\" besteht bereits", + "error.file.extension.forbidden": + "Verbotene Dateiendung \"{extension}\"", + "error.file.extension.missing": + "Du kannst keine Dateien ohne Dateiendung hochladen", + "error.file.maxheight": "Die Bildhöhe darf {height} Pixel nicht überschreiten", + "error.file.maxsize": "Die Datei ist zu groß", + "error.file.maxwidth": "Die Bildbreite darf {height} Pixel nicht überschreiten", + "error.file.mime.differs": + "Die Datei muss den Medientyp \"{mime}\" haben.", + "error.file.mime.forbidden": "Der Medientyp \"{mime}\" ist nicht erlaubt", + "error.file.mime.invalid": "Ungültiger Dateityp: {mime}", + "error.file.mime.missing": + "Der Medientyp für \"{filename}\" konnte nicht erkannt werden", + "error.file.minheight": "Die Bildhöhe muss mindestens {height} Pixel betragen", + "error.file.minsize": "Die Datei ist zu klein", + "error.file.minwidth": "Die Bildbreite muss mindestens {height} Pixel betragen", + "error.file.name.missing": "Bitte gib einen Dateinamen an", + "error.file.notFound": "Die Datei \"{filename}\" konnte nicht gefunden werden", + "error.file.orientation": "Das Bildformat ist ungültig. Erwartetes Format: \"{orientation}\"", + "error.file.type.forbidden": "Du kannst keinen {type}-Dateien hochladen", + "error.file.undefined": "Die Datei konnte nicht gefunden werden", + + "error.form.incomplete": "Bitte behebe alle Fehler …", + "error.form.notSaved": "Das Formular konnte nicht gespeichert werden", + + "error.language.code": "Bitte gib einen gültigen Code für die Sprache an", + "error.language.duplicate": "Die Sprache besteht bereits", + "error.language.name": "Bitte gib einen gültigen Namen für die Sprache an", + + "error.license.format": "Bitte gib einen gültigen Lizenzschlüssel ein", + "error.license.email": "Bitte gib eine gültige E-Mailadresse an", + "error.license.verification": "Die Lizenz konnte nicht verifiziert werden", + + "error.page.changeSlug.permission": + "Du darfst die URL der Seite \"{slug}\" nicht ändern", + "error.page.changeStatus.incomplete": + "Die Seite ist nicht vollständig und kann daher nicht veröffentlicht werden", + "error.page.changeStatus.permission": + "Der Status der Seite kann nicht geändert werden", + "error.page.changeStatus.toDraft.invalid": + "Die Seite \"{slug}\" kann nicht in einen Entwurf umgewandelt werden", + "error.page.changeTemplate.invalid": + "Die Vorlage für die Seite \"{slug}\" kann nicht geändert werden", + "error.page.changeTemplate.permission": + "Du kannst die Vorlage für die Seite \"{slug}\" nicht ändern", + "error.page.changeTitle.empty": "Bitte gib einen Titel an", + "error.page.changeTitle.permission": + "Du kannst den Titel für die Seite \"{slug}\" nicht ändern", + "error.page.create.permission": "Du kannst die Seite \"{slug}\" nicht anlegen", + "error.page.delete": "Die Seite \"{slug}\" kann nicht gelöscht werden", + "error.page.delete.confirm": "Bitte gib zur Bestätigung den Seitentitel ein", + "error.page.delete.hasChildren": + "Die Seite hat Unterseiten und kann nicht gelöscht werden", + "error.page.delete.permission": "Du kannst die Seite \"{slug}\" nicht löschen", + "error.page.draft.duplicate": + "Ein Entwurf mit dem URL-Kürzel \"{slug}\" besteht bereits", + "error.page.duplicate": + "Eine Seite mit dem URL-Kürzel \"{slug}\" besteht bereits", + "error.page.duplicate.permission": "Du kannst die Seite \"{slug}\" nicht duplizieren", + "error.page.notFound": "Die Seite \"{slug}\" konnte nicht gefunden werden", + "error.page.num.invalid": + "Bitte gib eine gültige Sortierungszahl an. Negative Zahlen sind nicht erlaubt.", + "error.page.slug.invalid": "Bitte gib ein gültiges URL-Kürzel an", + "error.page.sort.permission": "Die Seite \"{slug}\" kann nicht umsortiert werden", + "error.page.status.invalid": "Bitte gib einen gültigen Seitenstatus an", + "error.page.undefined": "Die Seite konnte nicht gefunden werden", + "error.page.update.permission": "Du kannst die Seite \"{slug}\" nicht editieren", + + "error.section.files.max.plural": + "Bitte füge nicht mehr als {max} Dateien zum Bereich \"{section}\" hinzu", + "error.section.files.max.singular": + "Bitte füge nicht mehr als eine Datei zum Bereich \"{section}\" hinzu", + "error.section.files.min.plural": + "Der Bereich \"{section}\" benötigt mindestens {min} Dateien", + "error.section.files.min.singular": + "Der Bereich \"{section}\" benötigt mindestens eine Datei", + + "error.section.pages.max.plural": + "Bitte füge nicht mehr als {max} Seiten zum Bereich \"{section}\" hinzu", + "error.section.pages.max.singular": + "Bitte füge nicht mehr als eine Seite zum Bereich \"{section}\" hinzu", + "error.section.pages.min.plural": + "Der Bereich \"{section}\" benötigt mindestens {min} Seiten", + "error.section.pages.min.singular": + "Der Bereich \"{section}\" benötigt mindestens eine Seite", + + "error.section.notLoaded": "Der Bereich \"{name}\" konnte nicht geladen werden", + "error.section.type.invalid": "Der Bereichstyp \"{type}\" ist nicht gültig", + + "error.site.changeTitle.empty": "Bitte gib einen Titel an", + "error.site.changeTitle.permission": + "Du kannst den Titel der Seite nicht ändern", + "error.site.update.permission": "Du darfst die Seite nicht editieren", + + "error.template.default.notFound": "Die \"Default\"-Vorlage existiert nicht", + + "error.user.changeEmail.permission": + "Du kannst die E-Mailadresse für den Benutzer \"{name}\" nicht ändern", + "error.user.changeLanguage.permission": + "Du kannst die Sprache für den Benutzer \"{name}\" nicht ändern", + "error.user.changeName.permission": + "Du kannst den Namen für den Benutzer \"{name}\" nicht ändern", + "error.user.changePassword.permission": + "Du kannst das Passwort für den Benutzer \"{name}\" nicht ändern", + "error.user.changeRole.lastAdmin": + "Die Rolle des letzten Administrators kann nicht geändert werden", + "error.user.changeRole.permission": + "Du kannst die Rolle für den Benutzer \"{name}\" nicht ändern", + "error.user.changeRole.toAdmin": + "Du darfst die Admin Rolle nicht an andere Benutzer vergeben", + "error.user.create.permission": "Du kannst diesen Benutzer nicht anlegen", + "error.user.delete": "Der Benutzer \"{name}\" konnte nicht gelöscht werden", + "error.user.delete.lastAdmin": "Du kannst den letzten Admin nicht l\u00f6schen", + "error.user.delete.lastUser": "Der letzte Benutzer kann nicht gelöscht werden", + "error.user.delete.permission": + "Du darfst den Benutzer \"{name}\" nicht löschen", + "error.user.duplicate": + "Ein Benutzer mit der E-Mailadresse \"{email}\" besteht bereits", + "error.user.email.invalid": "Bitte gib eine gültige E-Mailadresse an", + "error.user.language.invalid": "Bitte gib eine gültige Sprache an", + "error.user.notFound": "Der Benutzer \"{name}\" wurde nicht gefunden", + "error.user.password.invalid": + "Bitte gib ein gültiges Passwort ein. Passwörter müssen mindestens 8 Zeichen lang sein.", + "error.user.password.notSame": "Die Passwörter stimmen nicht überein", + "error.user.password.undefined": "Der Benutzer hat kein Passwort", + "error.user.role.invalid": "Bitte gib eine gültige Rolle an", + "error.user.update.permission": + "Du darfst den den Benutzer \"{name}\" nicht editieren", + + "error.validation.accepted": "Bitte bestätige", + "error.validation.alpha": "Bitte gib nur Zeichen zwischen A und Z ein", + "error.validation.alphanum": + "Bitte gib nur Zeichen zwischen A und Z und Zahlen zwischen 0 und 9 ein", + "error.validation.between": + "Bitte gib einen Wert zwischen \"{min}\" und \"{max}\" ein", + "error.validation.boolean": "Bitte bestätige oder lehne ab", + "error.validation.contains": + "Bitte gib einen Wert ein, der \"{needle}\" enthält", + "error.validation.date": "Bitte gib ein gültiges Datum ein", + "error.validation.date.after": "Bitte gib ein Datum nach dem {date} ein", + "error.validation.date.before": "Bitte gib ein Datum vor dem {date} ein", + "error.validation.date.between": "Bitte gib ein Datum zwischen dem {min} und dem {max} ein", + "error.validation.denied": "Bitte lehne die Eingabe ab", + "error.validation.different": "Der Wert darf nicht \"{other}\" sein", + "error.validation.email": "Bitte gib eine gültige E-Mailadresse an", + "error.validation.endswith": "Der Wert muss auf \"{end}\" enden", + "error.validation.filename": "Bitte gib einen gültigen Dateinamen ein", + "error.validation.in": "Bitte gib einen der folgenden Werte ein: ({in})", + "error.validation.integer": "Bitte gib eine ganze Zahl ein", + "error.validation.ip": "Bitte gib eine gültige IP Adresse ein", + "error.validation.less": "Bitte gib einen Wert kleiner als {max} ein", + "error.validation.match": "Der Wert entspricht nicht dem erwarteten Muster", + "error.validation.max": "Bitte gib einen Wert ein, der nicht größer als {max} ist", + "error.validation.maxlength": + "Bitte gib einen kürzeren Text ein (max. {max} Zeichen)", + "error.validation.maxwords": "Bitte nutze nicht mehr als {max} Wort(e)", + "error.validation.min": "Bitte gib einen Wert ein, der nicht kleiner als {min} ist", + "error.validation.minlength": + "Bitte gib einen längeren Text ein. (min. {min} Zeichen)", + "error.validation.minwords": "Bitte nutze mindestens {min} Wort(e)", + "error.validation.more": "Bitte gib einen größeren Wert als {min} ein", + "error.validation.notcontains": + "Bitte gib einen Wert ein, der nicht \"{needle}\" enthält", + "error.validation.notin": + "Bitte gib keinen der folgenden Werte ein: ({notIn})", + "error.validation.option": "Bitte wähle eine gültige Option aus", + "error.validation.num": "Bitte gib eine gültige Zahl an", + "error.validation.required": "Bitte gib etwas ein", + "error.validation.same": "Bitte gib \"{other}\" ein", + "error.validation.size": "Die Größe des Wertes muss \"{size}\" sein", + "error.validation.startswith": "Der Wert muss mit \"{start}\" beginnen", + "error.validation.time": "Bitte gib eine gültige Uhrzeit ein", + "error.validation.url": "Bitte gib eine gültige URL ein", + + "field.required": "Das Feld ist Pflicht", + "field.files.empty": "Keine Dateien ausgewählt", + "field.pages.empty": "Keine Seiten ausgewählt", + "field.structure.delete.confirm": "Willst du diesen Eintrag wirklich l\u00f6schen?", + "field.structure.empty": "Es bestehen keine Eintr\u00e4ge.", + "field.users.empty": "Keine Benutzer ausgewählt", + + "file.delete.confirm": + "Willst du die Datei {filename}
wirklich löschen?", + + "files": "Dateien", + "files.empty": "Keine Dateien", + + "hour": "Stunde", + "insert": "Einf\u00fcgen", + "install": "Installieren", + + "installation": "Installation", + "installation.completed": "Das Panel wurde installiert", + "installation.disabled": "Die Panel-Installation ist auf öffentlichen Servern automatisch deaktiviert. Bitte installiere das Panel auf einem lokalen Server oder aktiviere die Installation gezielt mit der panel.install Option. ", + "installation.issues.accounts": + "/site/accounts ist nicht beschreibbar", + "installation.issues.content": + "/content existiert nicht oder ist nicht beschreibbar", + "installation.issues.curl": "Die CURL Erweiterung wird benötigt", + "installation.issues.headline": "Das Panel kann nicht installiert werden", + "installation.issues.mbstring": + "Die MB String Erweiterung wird benötigt", + "installation.issues.media": + "Der /media Ordner ist nicht beschreibbar", + "installation.issues.php": "Bitte verwende PHP 7+", + "installation.issues.server": + "Kirby benötigt Apache, Nginx or Caddy", + "installation.issues.sessions": "/site/sessions ist nicht beschreibbar", + + "language": "Sprache", + "language.code": "Code", + "language.convert": "Als Standard auswählen", + "language.convert.confirm": + "

Willst du {name} wirklich in die Standardsprache umwandeln? Dieser Schritt kann nicht rückgängig gemacht werden.

Wenn {name} unübersetzte Felder hat, gibt es keine gültigen Standardwerte für diese Felder und Inhalte könnten verloren gehen.

", + "language.create": "Neue Sprache anlegen", + "language.delete.confirm": + "Willst du {name} inklusive aller Übersetzungen wirklich löschen? Dieser Schritt kann nicht rückgängig gemacht werden!", + "language.deleted": "Die Sprache wurde gelöscht", + "language.direction": "Leserichtung", + "language.direction.ltr": "Von links nach rechts", + "language.direction.rtl": "Von rechts nach links", + "language.locale": "PHP locale string", + "language.locale.warning": "Du nutzt ein angepasstes Setup for PHP Locales. Bitte bearbeite dieses direkt in der entsprechenden Sprachdatei in /site/languages", + "language.name": "Name", + "language.updated": "Die Sprache wurde gespeichert", + + "languages": "Sprachen", + "languages.default": "Standardsprache", + "languages.empty": "Noch keine Sprachen", + "languages.secondary": "Sekundäre Sprachen", + "languages.secondary.empty": "Noch keine sekundären Sprachen", + + "license": "Lizenz", + "license.buy": "Kaufe eine Lizenz", + "license.register": "Registrieren", + "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", + + "link": "Link", + "link.text": "Linktext", + + "loading": "Laden", + + "lock.unsaved": "Ungespeicherte Änderungen", + "lock.unsaved.empty": "Keine ungespeicherten Änderungen", + "lock.isLocked": "Ungespeicherte Änderungen von {email}", + "lock.file.isLocked": "Die Datei wird von {email} bearbeitet und kann nicht geändert werden.", + "lock.page.isLocked": "Die Seite wird von {email} bearbeitet und kann nicht geändert werden.", + "lock.unlock": "Entsperren", + "lock.isUnlocked": "Deine ungespeicherten Änderungen wurden von einem anderen Benutzer überschrieben. Du kannst sie herunterladen, um sie manuell einzufügen. ", + + "login": "Anmelden", + "login.remember": "Angemeldet bleiben", + + "logout": "Abmelden", + + "menu": "Menü", + "meridiem": "AM/PM", + "mime": "Medientyp", + "minutes": "Minuten", + + "month": "Monat", + "months.april": "April", + "months.august": "August", + "months.december": "Dezember", + "months.february": "Februar", + "months.january": "Januar", + "months.july": "Juli", + "months.june": "Juni", + "months.march": "M\u00e4rz", + "months.may": "Mai", + "months.november": "November", + "months.october": "Oktober", + "months.september": "September", + + "more": "Mehr", + "name": "Name", + "next": "Nächster Eintrag", + "off": "aus", + "on": "an", + "open": "Öffnen", + "options": "Optionen", + + "orientation": "Ausrichtung", + "orientation.landscape": "Querformat", + "orientation.portrait": "Hochformat", + "orientation.square": "Quadratisch", + + "page.changeSlug": "URL \u00e4ndern", + "page.changeSlug.fromTitle": "Aus Titel erzeugen", + "page.changeStatus": "Status ändern", + "page.changeStatus.position": "Bitte wähle eine Position aus", + "page.changeStatus.select": "Wähle einen neuen Status aus", + "page.changeTemplate": "Vorlage ändern", + "page.delete.confirm": + "Willst du die Seite {title} wirklich löschen?", + "page.delete.confirm.subpages": + "Diese Seite hat Unterseiten.
Alle Unterseiten werden ebenfalls gelöscht.", + "page.delete.confirm.title": "Gib zur Bestätigung den Seitentitel ein", + "page.draft.create": "Entwurf anlegen", + "page.duplicate.appendix": "Kopie", + "page.duplicate.files": "Dateien kopieren", + "page.duplicate.pages": "Seiten kopieren", + "page.status": "Status", + "page.status.draft": "Entwurf", + "page.status.draft.description": + "Die Seite ist im Entwurfsmodus und ist nur für angemeldete Benutzer sichtbar", + "page.status.listed": "Öffentlich", + "page.status.listed.description": "Die Seite ist öffentlich für alle Besucher", + "page.status.unlisted": "Ungelistet", + "page.status.unlisted.description": "Die Seite kann nur über die URL aufgerufen werden", + + "pages": "Seiten", + "pages.empty": "Keine Seiten", + "pages.status.draft": "Entwürfe", + "pages.status.listed": "Veröffentlicht", + "pages.status.unlisted": "Ungelistet", + + "pagination.page": "Seite", + + "password": "Passwort", + "pixel": "Pixel", + "prev": "Vorheriger Eintrag", + "remove": "Entfernen", + "rename": "Umbenennen", + "replace": "Ersetzen", + "retry": "Wiederholen", + "revert": "Verwerfen", + + "role": "Rolle", + "role.admin.description": "Administratoren haben alle Rechte", + "role.admin.title": "Administrator", + "role.all": "Alle", + "role.empty": "Keine Benutzer mit dieser Rolle", + "role.description.placeholder": "Keine Beschreibung", + "role.nobody.description": "Dies ist die Platzhalterrolle ohne Rechte", + "role.nobody.title": "Niemand", + + "save": "Speichern", + "search": "Suchen", + + "section.required": "Der Bereich ist Pflicht", + + "select": "Auswählen", + "settings": "Einstellungen", + "size": "Größe", + "slug": "URL-Anhang", + "sort": "Sortieren", + "title": "Titel", + "template": "Vorlage", + "today": "Heute", + + "toolbar.button.code": "Code", + "toolbar.button.bold": "Fetter Text", + "toolbar.button.email": "E-Mail", + "toolbar.button.headings": "Überschriften", + "toolbar.button.heading.1": "Überschrift 1", + "toolbar.button.heading.2": "Überschrift 2", + "toolbar.button.heading.3": "Überschrift 3", + "toolbar.button.italic": "Kursiver Text", + "toolbar.button.file": "Datei", + "toolbar.button.file.select": "Datei auswählen", + "toolbar.button.file.upload": "Datei hochladen", + "toolbar.button.link": "Link", + "toolbar.button.ol": "Geordnete Liste", + "toolbar.button.ul": "Ungeordnete Liste", + + "translation.author": "Kirby Team", + "translation.direction": "ltr", + "translation.name": "Deutsch", + "translation.locale": "de_DE", + + "upload": "Hochladen", + "upload.error.cantMove": "Die Datei konnte nicht an ihren Zielort bewegt werden", + "upload.error.cantWrite": "Die Datei konnte nicht auf der Festplatte gespeichert werden", + "upload.error.default": "Die Datei konnte nicht hochgeladen werden", + "upload.error.extension": "Der Dateiupload wurde durch eine Erweiterung verhindert", + "upload.error.formSize": "Die Datei ist größer als die MAX_FILE_SIZE Einstellung im Formular", + "upload.error.iniPostSize": "Die Datei ist größer als die post_max_size Einstellung in der php.ini", + "upload.error.iniSize": "Die Datei ist größer als die upload_max_filesize Einstellung in der php.ini", + "upload.error.noFile": "Es wurde keine Datei hochgeladen", + "upload.error.noFiles": "Es wurden keine Dateien hochgeladen", + "upload.error.partial": "Die Datei wurde nur teilweise hochgeladen", + "upload.error.tmpDir": "Der temporäre Ordner für den Dateiupload existiert leider nicht", + "upload.errors": "Fehler", + "upload.progress": "Hochladen …", + + "url": "Url", + "url.placeholder": "https://beispiel.de", + + "user": "Benutzer", + "user.blueprint": + "Du kannst zusätzliche Felder und Bereiche für diese Benutzerrolle in /site/blueprints/users/{role}.yml anlegen", + "user.changeEmail": "E-Mail ändern", + "user.changeLanguage": "Sprache ändern", + "user.changeName": "Benutzer umbenennen", + "user.changePassword": "Passwort ändern", + "user.changePassword.new": "Neues Passwort", + "user.changePassword.new.confirm": "Wiederhole das Passwort …", + "user.changeRole": "Rolle ändern", + "user.changeRole.select": "Neue Rolle auswählen", + "user.create": "Neuen Benutzer anlegen", + "user.delete": "Benutzer löschen", + "user.delete.confirm": + "Willst du den Benutzer
{email} wirklich löschen?", + + "users": "Benutzer", + + "version": "Version", + + "view.account": "Dein Account", + "view.installation": "Installation", + "view.settings": "Einstellungen", + "view.site": "Seite", + "view.users": "Benutzer", + + "welcome": "Willkommen", + "year": "Jahr" +} diff --git a/kirby/i18n/translations/el.json b/kirby/i18n/translations/el.json new file mode 100755 index 0000000..d7b08eb --- /dev/null +++ b/kirby/i18n/translations/el.json @@ -0,0 +1,481 @@ +{ + "add": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7", + "avatar": "\u0395\u03b9\u03ba\u03cc\u03bd\u03b1 \u03c0\u03c1\u03bf\u03c6\u03af\u03bb", + "back": "Πίσω", + "cancel": "\u0391\u03ba\u03cd\u03c1\u03c9\u03c3\u03b7", + "change": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ae", + "close": "\u039a\u03bb\u03b5\u03af\u03c3\u03b9\u03bc\u03bf", + "confirm": "Εντάξει", + "copy": "Αντιγραφή", + "create": "Δημιουργία", + + "date": "Ημερομηνία", + "date.select": "Επιλογή ημερομηνίας", + + "day": "Ημέρα", + "days.fri": "\u03a0\u03b1\u03c1", + "days.mon": "\u0394\u03b5\u03c5", + "days.sat": "\u03a3\u03ac\u03b2", + "days.sun": "\u039a\u03c5\u03c1", + "days.thu": "\u03a0\u03ad\u03bc", + "days.tue": "\u03a4\u03c1\u03af", + "days.wed": "\u03a4\u03b5\u03c4", + + "delete": "\u0394\u03b9\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae", + "dimensions": "Διαστάσεις", + "disabled": "Disabled", + "discard": "Απόρριψη", + "download": "Download", + "duplicate": "Duplicate", + "edit": "\u0395\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b1", + + "dialog.files.empty": "No files to select", + "dialog.pages.empty": "No pages to select", + "dialog.users.empty": "No users to select", + + "email": "Διεύθυνση ηλεκτρονικού ταχυδρομείου", + "email.placeholder": "mail@example.com", + + "error.access.login": "Mη έγκυρη σύνδεση", + "error.access.panel": "Δεν επιτρέπεται η πρόσβαση στον πίνακα ελέγχου", + "error.access.view": "You are not allowed to access this part of the panel", + + "error.avatar.create.fail": "Δεν ήταν δυνατή η μεταφόρτωση της εικόνας προφίλ", + "error.avatar.delete.fail": "Δεν ήταν δυνατή η διαγραφή της εικόνας προφίλ", + "error.avatar.dimensions.invalid": + "Διατηρήστε το πλάτος και το ύψος της εικόνας προφίλ κάτω από 3000 εικονοστοιχεία", + "error.avatar.mime.forbidden": + "\u039c\u03b7 \u03b1\u03c0\u03bf\u03b4\u03b5\u03ba\u03c4\u03cc\u03c2 \u03c4\u03cd\u03c0\u03bf\u03c2 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5", + + "error.blueprint.notFound": "Δεν ήταν δυνατή η φόρτωση του προσχεδίου \"{name}\"", + + "error.email.preset.notFound": "Δεν είναι δυνατή η εύρεση της προεπιλογής διεύθινσης ηλεκτρονικού ταχυδρομείου \"{name}\"", + + "error.field.converter.invalid": "Μη έγκυρος μετατροπέας \"{converter}\"", + + "error.file.changeName.empty": "The name must not be empty", + "error.file.changeName.permission": + "Δεν επιτρέπεται να αλλάξετε το όνομα του \"{filename}\"", + "error.file.duplicate": "Ένα αρχείο με το όνομα \"{filename}\" υπάρχει ήδη", + "error.file.extension.forbidden": + "\u039c\u03b7 \u03b1\u03c0\u03bf\u03b4\u03b5\u03ba\u03c4\u03ae \u03b5\u03c0\u03ad\u03ba\u03c4\u03b1\u03c3\u03b7 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5", + "error.file.extension.missing": + "Λείπει η επέκταση για το \"{filename}\"", + "error.file.maxheight": "The height of the image must not exceed {height} pixels", + "error.file.maxsize": "The file is too large", + "error.file.maxwidth": "The width of the image must not exceed {width} pixels", + "error.file.mime.differs": + "Το αρχείο πρέπει να είναι του ίδιου τύπου mime \"{mime}\"", + "error.file.mime.forbidden": "Ο τύπος μέσου \"{mime}\" δεν επιτρέπεται", + "error.file.mime.invalid": "Invalid mime type: {mime}", + "error.file.mime.missing": + "Δεν είναι δυνατό να εντοπιστεί ο τύπος μέσου για το \"{filename}\"", + "error.file.minheight": "The height of the image must be at least {height} pixels", + "error.file.minsize": "The file is too small", + "error.file.minwidth": "The width of the image must be at least {width} pixels", + "error.file.name.missing": "Το όνομα αρχείου δεν μπορεί να είναι άδειο", + "error.file.notFound": "Δεν είναι δυνατό να βρεθεί το αρχείο \"{filename}\"", + "error.file.orientation": "The orientation of the image must be \"{orientation}\"", + "error.file.type.forbidden": "Δεν επιτρέπεται η μεταφόρτωση αρχείων {type}", + "error.file.undefined": "Δεν ήταν δυνατή η εύρεση του αρχείου", + + "error.form.incomplete": "Παρακαλώ διορθώστε τα σφάλματα στη φόρμα...", + "error.form.notSaved": "Δεν ήταν δυνατή η αποθήκευση της φόρμας", + + "error.language.code": "Please enter a valid code for the language", + "error.language.duplicate": "The language already exists", + "error.language.name": "Please enter a valid name for the language", + + "error.license.format": "Please enter a valid license key", + "error.license.email": "Παρακαλώ εισάγετε μια έγκυρη διεύθυνση ηλεκτρονικού ταχυδρομείου", + "error.license.verification": "The license could not be verified", + + "error.page.changeSlug.permission": + "Δεν επιτρέπεται να αλλάξετε το URL της σελίδας \"{slug}\"", + "error.page.changeStatus.incomplete": + "Δεν ήταν δυνατή η δημοσίευση της σελίδας καθώς περιέχει σφάλματα", + "error.page.changeStatus.permission": + "Δεν είναι δυνατή η αλλαγή κατάστασης για αυτή τη σελίδα", + "error.page.changeStatus.toDraft.invalid": + "Δεν είναι δυνατή η μετατροπή της σελίδας \"{slug}\" σε προσχέδιο", + "error.page.changeTemplate.invalid": + "Δεν είναι δυνατή η αλλαγή προτύπου για τη σελίδα \"{slug}\"", + "error.page.changeTemplate.permission": + "Δεν επιτρέπεται να αλλάξετε το πρότυπο για τη σελίδα \"{slug}\"", + "error.page.changeTitle.empty": "Ο τίτλος δεν μπορεί να είναι κενός", + "error.page.changeTitle.permission": + "Δεν επιτρέπεται να αλλάξετε τον τίτλο για τη σελίδα \"{slug}\"", + "error.page.create.permission": "Δεν επιτρέπεται να δημιουργήσετε τη σελίδα \"{slug}\"", + "error.page.delete": "Δεν είναι δυνατή η διαγραφή της σελίδας \"{slug}\"", + "error.page.delete.confirm": "Παρακαλώ εισάγετε τον τίτλο της σελίδας για επιβεβαίωση", + "error.page.delete.hasChildren": + "Δεν είναι δυνατή η διαγραφή της σελίδας καθώς περιέχει υποσελίδες", + "error.page.delete.permission": "Δεν επιτρέπεται η διαγραφή της σελίδας \"{slug}\"", + "error.page.draft.duplicate": + "Υπάρχει ήδη ένα προσχέδιο σελίδας με την διεύθυνση URL \"{slug}\"", + "error.page.duplicate": + "Υπάρχει ήδη μια σελίδα με την διεύθυνση URL \"{slug}\"", + "error.page.duplicate.permission": "You are not allowed to duplicate \"{slug}\"", + "error.page.notFound": "Δεν ήταν δυνατή η εύρεση της σελίδας \"{slug}\"", + "error.page.num.invalid": + "Παρακαλώ εισάγετε έναν έγκυρο αριθμό ταξινόμησης. Οι αριθμοί δεν μπορεί να είναι αρνητικοί.", + "error.page.slug.invalid": "Παρακαλώ εισάγετε ένα έγκυρο πρόθεμα διεύθυνσης URL", + "error.page.sort.permission": "Δεν είναι δυνατή η ταξινόμηση της σελίδας \"{slug}\"", + "error.page.status.invalid": "Ορίστε μια έγκυρη κατάσταση σελίδας", + "error.page.undefined": "Δεν ήταν δυνατή η εύρεση της σελίδας", + "error.page.update.permission": "Δεν επιτρέπεται η ενημέρωση της σελίδας \"{slug}\"", + + "error.section.files.max.plural": + "Δεν πρέπει να προσθέσετε περισσότερα από {max} αρχεία στην ενότητα \"{section}\"", + "error.section.files.max.singular": + "Δεν πρέπει να προσθέσετε περισσότερα από ένα αρχεία στην ενότητα \"{section}\"", + "error.section.files.min.plural": + "The \"{section}\" section requires at least {min} files", + "error.section.files.min.singular": + "The \"{section}\" section requires at least one file", + + "error.section.pages.max.plural": + "Δεν μπορείτε να προσθέσετε περισσότερες από {max} σελίδες στην ενότητα \"{section}\"", + "error.section.pages.max.singular": + "Δεν μπορείτε να προσθέσετε περισσότερες από μία σελίδες στην ενότητα \"{section}\"", + "error.section.pages.min.plural": + "The \"{section}\" section requires at least {min} pages", + "error.section.pages.min.singular": + "The \"{section}\" section requires at least one page", + + "error.section.notLoaded": "Δεν ήταν δυνατή η φόρτωση της ενότητας \"{name}\"", + "error.section.type.invalid": "Ο τύπος ενότητας \"{type}\" δεν είναι έγκυρος", + + "error.site.changeTitle.empty": "Ο τίτλος δεν μπορεί να είναι κενός", + "error.site.changeTitle.permission": + "Δεν επιτρέπεται να αλλάξετε τον τίτλο του ιστότοπου", + "error.site.update.permission": "Δεν επιτρέπεται η ενημέρωση του ιστότοπου", + + "error.template.default.notFound": "Το προεπιλεγμένο πρότυπο δεν υπάρχει", + + "error.user.changeEmail.permission": + "Δεν επιτρέπεται να αλλάξετε τη διεύθινση ηλεκτρονικού ταχυδρομείου για τον χρήστη \"{name}\"", + "error.user.changeLanguage.permission": + "Δεν επιτρέπεται να αλλάξετε τη γλώσσα για τον χρήστη \"{name}\"", + "error.user.changeName.permission": + "Δεν επιτρέπεται να αλλάξετε το όνομα του χρήστη \"{name}", + "error.user.changePassword.permission": + "Δεν επιτρέπεται να αλλάξετε τον κωδικό πρόσβασης για τον χρήστη \"{name}\"", + "error.user.changeRole.lastAdmin": + "Ο ρόλος του τελευταίου διαχειριστή δεν μπορεί να αλλάξει", + "error.user.changeRole.permission": + "Δεν επιτρέπεται να αλλάξετε το ρόλο του χρήστη \"{name}\"", + "error.user.changeRole.toAdmin": + "You are not allowed to promote someone to the admin role", + "error.user.create.permission": "Δεν επιτρέπεται η δημιουργία αυτού του χρήστη", + "error.user.delete": "\u039f \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03bf\u03cd\u03c3\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03b3\u03c1\u03b1\u03c6\u03b5\u03af", + "error.user.delete.lastAdmin": "Δεν είναι δυνατή η διαγραφή του τελευταίου διαχειριστή", + "error.user.delete.lastUser": "Δεν είναι δυνατή η διαγραφή του τελευταίου χρήστη", + "error.user.delete.permission": + "Δεν επιτρέπεται να διαγράψετ τον χρήστη \"{name}\"", + "error.user.duplicate": + "Ένας χρήστης με τη διεύθυνση ηλεκτρονικού ταχυδρομείου \"{email}\" υπάρχει ήδη", + "error.user.email.invalid": "Παρακαλώ εισάγετε μια έγκυρη διεύθυνση ηλεκτρονικού ταχυδρομείου", + "error.user.language.invalid": "Παρακαλώ εισαγάγετε μια έγκυρη γλώσσα", + "error.user.notFound": "Δεν είναι δυνατή η εύρεση του χρήστη \"{name}\"", + "error.user.password.invalid": + "Παρακαλώ εισάγετε έναν έγκυρο κωδικό πρόσβασης. Οι κωδικοί πρόσβασης πρέπει να έχουν μήκος τουλάχιστον 8 χαρακτήρων.", + "error.user.password.notSame": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03b9\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u039a\u03c9\u03b4\u03b9\u03ba\u03cc \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "error.user.password.undefined": "Ο χρήστης δεν έχει κωδικό πρόσβασης", + "error.user.role.invalid": "Παρακαλώ εισαγάγετε έναν έγκυρο ρόλο", + "error.user.update.permission": + "Δεν επιτρέπεται η ενημέρωση του χρήστη \"{name}\"", + + "error.validation.accepted": "Παρακαλώ επιβεβαιώστε", + "error.validation.alpha": "Παρακαλώ εισάγετε μόνο χαρακτήρες μεταξύ των a-z", + "error.validation.alphanum": + "Παρακαλώ εισάγετε μόνο χαρακτήρες μεταξύ των a-z ή αριθμούς απο το 0 έως το 9", + "error.validation.between": + "Παρακαλώ εισάγετε μια τιμή μεταξύ \"{min}\" και \"{max}\"", + "error.validation.boolean": "Παρακαλώ επιβεβαιώστε ή αρνηθείτε", + "error.validation.contains": + "Παρακαλώ καταχωρίστε μια τιμή που περιέχει \"{needle}\"", + "error.validation.date": "Παρακαλώ εισάγετε μία έγκυρη ημερομηνία", + "error.validation.date.after": "Please enter a date after {date}", + "error.validation.date.before": "Please enter a date before {date}", + "error.validation.date.between": "Please enter a date between {min} and {max}", + "error.validation.denied": "Παρακαλώ αρνηθείτε", + "error.validation.different": "Η τιμή δεν μπορεί να είναι \"{other}\"", + "error.validation.email": "Παρακαλώ εισάγετε μια έγκυρη διεύθυνση ηλεκτρονικού ταχυδρομείου", + "error.validation.endswith": "Η τιμή πρέπει να τελειώνει με \"{end}\"", + "error.validation.filename": "Παρακαλώ εισάγετε ένα έγκυρο όνομα αρχείου", + "error.validation.in": "Παρακαλώ εισάγετε ένα από τα παρακάτω: ({in})", + "error.validation.integer": "Παρακαλώ εισάγετε έναν έγκυρο ακέραιο αριθμό", + "error.validation.ip": "Παρακαλώ εισάγετε μια έγκυρη διεύθυνση IP", + "error.validation.less": "Παρακαλώ εισάγετε μια τιμή μικρότερη από {max}", + "error.validation.match": "Η τιμή δεν ταιριάζει με το αναμενόμενο πρότυπο", + "error.validation.max": "Παρακαλώ εισάγετε μια τιμή ίση ή μικρότερη από {max}", + "error.validation.maxlength": + "Παρακαλώ εισάγετε μια μικρότερη τιμή. (max. {max} χαρακτήρες)", + "error.validation.maxwords": "Παρακαλώ εισάγετε το πολύ {max} λέξεις", + "error.validation.min": "Παρακαλώ εισάγετε μια τιμή ίση ή μεγαλύτερη από {min}", + "error.validation.minlength": + "Παρακαλώ εισάγετε μεγαλύτερη τιμή. (τουλάχιστον {min} χαρακτήρες)", + "error.validation.minwords": "Παρακαλώ εισάγετε τουλάχιστον {min} λέξεις", + "error.validation.more": "Παρακαλώ εισάγετε τουλάχιστον {min} λέξεις", + "error.validation.notcontains": + "Παρακαλώ εισάγετε μια τιμή που δεν περιέχει \"{needle}\"", + "error.validation.notin": + "Παρακαλώ μην εισάγετε κανένα από τα παρακάτω: ({notIn})", + "error.validation.option": "Παρακαλώ κάντε μια έγκυρη επιλογή", + "error.validation.num": "Παρακαλώ εισάγετε έναν έγκυρο αριθμό", + "error.validation.required": "Παρακαλώ εισάγετε κάτι", + "error.validation.same": "Παρακαλώ εισάγετε \"{other}\"", + "error.validation.size": "Το μέγεθος της τιμής πρέπει να είναι \"{size}\"", + "error.validation.startswith": "Η τιμή πρέπει να αρχίζει με \"{start}\"", + "error.validation.time": "Παρακαλώ εισάγετε μια έγκυρη ώρα", + "error.validation.url": "Παρακαλώ εισάγετε μια έγκυρη διεύθυνση URL", + + "field.required": "The field is required", + "field.files.empty": "Δεν έχουν επιλεγεί αρχεία ακόμα", + "field.pages.empty": "Δεν έχουν επιλεγεί ακόμη σελίδες", + "field.structure.delete.confirm": "\u0395\u03af\u03c3\u03c4\u03b5 \u03c3\u03af\u03b3\u03bf\u03c5\u03c1\u03bf\u03c2 \u03cc\u03c4\u03b9 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03b3\u03c1\u03ac\u03c8\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b9\u03c3\u03b7;", + "field.structure.empty": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03b1\u03ba\u03cc\u03bc\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03af\u03c3\u03b5\u03b9\u03c2.", + "field.users.empty": "Δεν έχουν επιλεγεί ακόμη χρήστες", + + "file.delete.confirm": + "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03c3\u03af\u03b3\u03bf\u03c5\u03c1\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03b3\u03c1\u03ac\u03c8\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf;", + + "files": "Αρχεία", + "files.empty": "Δεν υπάρχουν ακόμα αρχεία", + + "hour": "Ώρα", + "insert": "\u0395\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae", + "install": "Εγκατάσταση", + + "installation": "Εγκατάσταση", + "installation.completed": "Ο πίνακας ελέγχου έχει εγκατασταθεί", + "installation.disabled": "Η εγκατάσταση του πίνακα ελέγχου είναι απενεργοποιημένη για δημόσιους διακομιστές από προεπιλογή. Εκτελέστε την εγκατάσταση σε ένα τοπικό μηχάνημα ή ενεργοποιήστε την με την επιλογή panel.install.", + "installation.issues.accounts": + "\u039f \u03c6\u03ac\u03ba\u03b5\u03bb\u03bf\u03c2 \/site\/accounts \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03b3\u03b3\u03c1\u03ac\u03c8\u03b9\u03bc\u03bf\u03c2", + "installation.issues.content": + "\u039f \u03c6\u03ac\u03ba\u03b5\u03bb\u03bf\u03c2 content \u03ba\u03b1\u03b9 \u03cc\u03bb\u03bf\u03b9 \u03bf\u03b9 \u03c5\u03c0\u03bf\u03c6\u03ac\u03ba\u03b5\u03bb\u03bf\u03b9 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03b3\u03b3\u03c1\u03ac\u03c8\u03b9\u03bc\u03bf\u03b9.", + "installation.issues.curl": "Απαιτείται η επέκταση CURL", + "installation.issues.headline": "Ο πίνακας ελέγχου δεν μπορεί να εγκατασταθεί", + "installation.issues.mbstring": + "Απαιτείται η επέκταση MB String ", + "installation.issues.media": + "Ο φάκελος /media δεν υπάρχει ή δεν είναι εγγράψιμος", + "installation.issues.php": "Βεβαιωθείτε ότι χρησιμοποιήτε PHP 7+", + "installation.issues.server": + "To Kirby απαιτεί Apache, Nginx ή Caddy", + "installation.issues.sessions": "Ο φάκελος /site/sessions δεν υπάρχει ή δεν είναι εγγράψιμος", + + "language": "\u0393\u03bb\u03ce\u03c3\u03c3\u03b1", + "language.code": "Κώδικας", + "language.convert": "Χρήση ως προεπιλογή", + "language.convert.confirm": + "

Θέλετε πραγματικά να μετατρέψετε τη {name} στην προεπιλεγμένη γλώσσα; Αυτό δεν μπορεί να ανακληθεί.

Αν το {name} χει μη μεταφρασμένο περιεχόμενο, δεν θα υπάρχει πλέον έγκυρη εναλλακτική λύση και τμήματα του ιστότοπού σας ενδέχεται να είναι κενά.

", + "language.create": "Προσθέστε μια νέα γλώσσα", + "language.delete.confirm": + "Θέλετε πραγματικά να διαγράψετε τη γλώσσα {name} συμπεριλαμβανομένων όλων των μεταφράσεων; Αυτό δεν μπορεί να αναιρεθεί!", + "language.deleted": "Η γλώσσα έχει διαγραφεί", + "language.direction": "Κατεύθυνση ανάγνωσης", + "language.direction.ltr": "Αριστερά προς τα δεξιά", + "language.direction.rtl": "Δεξιά προς τα αριστερά", + "language.locale": "Συμβολοσειρά τοπικής γλώσσας PHP", + "language.locale.warning": "You are using a custom locale set up. Please modify it in the language file in /site/languages", + "language.name": "Ονομασία", + "language.updated": "Η γλώσσα έχει ενημερωθεί", + + "languages": "Γλώσσες", + "languages.default": "Προεπιλεγμένη γλώσσα", + "languages.empty": "Δεν υπάρχουν ακόμη γλώσσες", + "languages.secondary": "Δευτερεύουσες γλώσσες", + "languages.secondary.empty": "Δεν υπάρχουν ακόμα δευτερεύουσες γλώσσες", + + "license": "\u0386\u03b4\u03b5\u03b9\u03b1 \u03a7\u03c1\u03ae\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Kirby", + "license.buy": "Αγοράστε μια άδεια", + "license.register": "Εγγραφή", + "license.register.help": + "Έχετε λάβει τον κωδικό άδειας χρήσης μετά την αγορά μέσω ηλεκτρονικού ταχυδρομείου. Παρακαλώ αντιγράψτε και επικολλήστε τον για να εγγραφείτε.", + "license.register.label": "Παρακαλώ εισαγάγετε τον κωδικό άδειας χρήσης", + "license.register.success": "Σας ευχαριστούμε για την υποστήριξη του Kirby", + "license.unregistered": "Αυτό είναι ένα μη καταχωρημένο demo του Kirby", + + "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", + + "loading": "Φόρτωση", + + "lock.unsaved": "Unsaved changes", + "lock.unsaved.empty": "There are no more unsaved changes", + "lock.isLocked": "Unsaved changes by {email}", + "lock.file.isLocked": "The file is currently being edited by {email} and cannot be changed.", + "lock.page.isLocked": "The page is currently being edited by {email} and cannot be changed.", + "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.remember": "Κρατήστε με συνδεδεμένο", + + "logout": "\u0391\u03c0\u03bf\u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7", + + "menu": "Μενού", + "meridiem": "Π.Μ./Μ.Μ", + "mime": "Τύπος πολυμέσων", + "minutes": "Λεπτά", + + "month": "Μήνας", + "months.april": "\u0391\u03c0\u03c1\u03af\u03bb\u03b9\u03bf\u03c2", + "months.august": "\u0391\u03cd\u03b3\u03bf\u03c5\u03c3\u03c4\u03bf\u03c2", + "months.december": "\u0394\u03b5\u03ba\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2", + "months.february": "\u03a6\u03b5\u03b2\u03c1\u03bf\u03c5\u03ac\u03c1\u03b9\u03bf\u03c2", + "months.january": "\u0399\u03b1\u03bd\u03bf\u03c5\u03ac\u03c1\u03b9\u03bf\u03c2", + "months.july": "\u0399\u03bf\u03cd\u03bb\u03b9\u03bf\u03c2", + "months.june": "\u0399\u03bf\u03cd\u03bd\u03b9\u03bf\u03c2", + "months.march": "\u039c\u03ac\u03c1\u03c4\u03b9\u03bf\u03c2", + "months.may": "\u039c\u03ac\u03b9\u03bf\u03c2", + "months.november": "\u039d\u03bf\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2", + "months.october": "\u039f\u03ba\u03c4\u03ce\u03b2\u03c1\u03b9\u03bf\u03c2", + "months.september": "\u03a3\u03b5\u03c0\u03c4\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2", + + "more": "Περισσότερα", + "name": "Ονομασία", + "next": "Επόμενο", + "off": "off", + "on": "on", + "open": "Άνοιγμα", + "options": "Eπιλογές", + + "orientation": "Προσανατολισμός", + "orientation.landscape": "Οριζόντιος", + "orientation.portrait": "Κάθετος", + "orientation.square": "Τετράγωνος", + + "page.changeSlug": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ae URL", + "page.changeSlug.fromTitle": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03c4\u03af\u03c4\u03bb\u03bf", + "page.changeStatus": "Αλλαγή κατάστασης", + "page.changeStatus.position": "Επιλέξτε μια θέση", + "page.changeStatus.select": "Επιλέξτε μια νέα κατάσταση", + "page.changeTemplate": "Αλλαγή προτύπου", + "page.delete.confirm": + "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03c3\u03af\u03b3\u03bf\u03c5\u03c1\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03b3\u03c1\u03ac\u03c8\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03b5\u03bb\u03af\u03b4\u03b1;", + "page.delete.confirm.subpages": + "Αυτή η σελίδα έχει υποσελίδες.
Όλες οι υποσελίδες θα διαγραφούν επίσης.", + "page.delete.confirm.title": "Εισάγετε τον τίτλο της σελίδας για επιβεβαίωση", + "page.draft.create": "Δημιουργία προσχεδίου", + "page.duplicate.appendix": "Αντιγραφή", + "page.duplicate.files": "Copy files", + "page.duplicate.pages": "Copy pages", + "page.status": "Kατάσταση", + "page.status.draft": "Προσχέδιο", + "page.status.draft.description": + "Η σελίδα είναι σε κατάσταση προσχεδίου και είναι ορατή μόνο για συνδεδεμένους συντάκτες", + "page.status.listed": "Δημοσιευμένο", + "page.status.listed.description": "Αυτή η σελίδα είναι δημοσιευμένη για οποιονδήποτε", + "page.status.unlisted": "Μη καταχωρημένο", + "page.status.unlisted.description": "Η σελίδα είναι προσβάσιμη μόνο μέσω της διεύθυνσης URL", + + "pages": "Σελίδες", + "pages.empty": "Δεν υπάρχουν ακόμα σελίδες", + "pages.status.draft": "Προσχέδια", + "pages.status.listed": "Δημοσιευμένο", + "pages.status.unlisted": "Μη καταχωρημένο", + + "pagination.page": "Σελίδα", + + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "pixel": "Εικονοστοιχέιο", + "prev": "Προηγούμενο", + "remove": "Αφαίρεση", + "rename": "Μετονομασία", + "replace": "\u0391\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7", + "retry": "\u0395\u03c0\u03b1\u03bd\u03ac\u03bb\u03b7\u03c8\u03b7", + "revert": "\u0391\u03b3\u03bd\u03cc\u03b7\u03c3\u03b7", + + "role": "\u03a1\u03cc\u03bb\u03bf\u03c2", + "role.admin.description": "The admin has all rights", + "role.admin.title": "Admin", + "role.all": "Όλα", + "role.empty": "Δεν υπάρχουν χρήστες με αυτόν τον ρόλο", + "role.description.placeholder": "Χωρίς περιγραφή", + "role.nobody.description": "This is a fallback role without any permissions", + "role.nobody.title": "Nobody", + + "save": "\u0391\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7", + "search": "Αναζήτηση", + + "section.required": "The section is required", + + "select": "Επιλογή", + "settings": "Ρυθμίσεις", + "size": "Μέγεθος", + "slug": "\u0395\u03c0\u03af\u03b8\u03b5\u03bc\u03b1 URL", + "sort": "Ταξινόμηση", + "title": "Τίτλος", + "template": "\u03a0\u03c1\u03cc\u03c4\u03c5\u03c0\u03bf", + "today": "Σήμερα", + + "toolbar.button.code": "Κώδικας", + "toolbar.button.bold": "\u0388\u03bd\u03c4\u03bf\u03bd\u03b7 \u03b3\u03c1\u03b1\u03c6\u03ae", + "toolbar.button.email": "Email", + "toolbar.button.headings": "Επικεφαλίδες", + "toolbar.button.heading.1": "Επικεφαλίδα 1", + "toolbar.button.heading.2": "Επικεφαλίδα 2", + "toolbar.button.heading.3": "Επικεφαλίδα 3", + "toolbar.button.italic": "\u03a0\u03bb\u03ac\u03b3\u03b9\u03b1 \u03b3\u03c1\u03b1\u03c6\u03ae", + "toolbar.button.file": "Αρχείο", + "toolbar.button.file.select": "Select a file", + "toolbar.button.file.upload": "Upload a file", + "toolbar.button.link": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf\u03c2", + "toolbar.button.ol": "Ταξινομημένη λίστα", + "toolbar.button.ul": "Λίστα κουκκίδων", + + "translation.author": "Ομάδα Kirby", + "translation.direction": "ltr", + "translation.name": "\u0395\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac", + "translation.locale": "el_GR", + + "upload": "Μεταφόρτωση", + "upload.error.cantMove": "The uploaded file could not be moved", + "upload.error.cantWrite": "Failed to write file to disk", + "upload.error.default": "The file could not be uploaded", + "upload.error.extension": "File upload stopped by extension", + "upload.error.formSize": "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the form", + "upload.error.iniPostSize": "The uploaded file exceeds the post_max_size directive in php.ini", + "upload.error.iniSize": "The uploaded file exceeds the upload_max_filesize directive in php.ini", + "upload.error.noFile": "No file was uploaded", + "upload.error.noFiles": "No files were uploaded", + "upload.error.partial": "The uploaded file was only partially uploaded", + "upload.error.tmpDir": "Missing a temporary folder", + "upload.errors": "Σφάλμα", + "upload.progress": "Μεταφόρτωση...", + + "url": "Διεύθινση url", + "url.placeholder": "https://example.com", + + "user": "Χρήστης", + "user.blueprint": + "Μπορείτε να ορίσετε επιπλέον τμήματα και πεδία φόρμας για αυτόν τον ρόλο χρήστη στο /site/blueprints/users/{role}.yml", + "user.changeEmail": "Αλλαγή διεύθινσης ηλεκτρονικού ταχυδρομείου", + "user.changeLanguage": "Αλλαγή γλώσσας", + "user.changeName": "Μετονομασία χρήστη", + "user.changePassword": "Αλλαγή κωδικού πρόσβασης", + "user.changePassword.new": "Νέος Κωδικός Πρόσβασης", + "user.changePassword.new.confirm": "Επαληθεύση κωδικού πρόσβασης", + "user.changeRole": "Αλλαγή ρόλου", + "user.changeRole.select": "Επιλογή νέου ρόλου", + "user.create": "Προσθήκη νέου χρήστη", + "user.delete": "Διαγραφή χρήστη", + "user.delete.confirm": + "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03c3\u03af\u03b3\u03bf\u03c5\u03c1\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03b3\u03c1\u03ac\u03c8\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7;", + + "users": "Χρήστες", + + "version": "\u0388\u03ba\u03b4\u03bf\u03c3\u03b7 Kirby", + + "view.account": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03c3\u03b1\u03c2", + "view.installation": "\u0395\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7", + "view.settings": "Ρυθμίσεις", + "view.site": "Iστοσελίδα", + "view.users": "\u03a7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2", + + "welcome": "Καλώς ήρθατε", + "year": "Έτος" +} diff --git a/kirby/i18n/translations/en.json b/kirby/i18n/translations/en.json new file mode 100755 index 0000000..173dc0c --- /dev/null +++ b/kirby/i18n/translations/en.json @@ -0,0 +1,481 @@ +{ + "add": "Add", + "avatar": "Profile picture", + "back": "Back", + "cancel": "Cancel", + "change": "Change", + "close": "Close", + "confirm": "Ok", + "copy": "Copy", + "create": "Create", + + "date": "Date", + "date.select": "Select a date", + + "day": "Day", + "days.fri": "Fri", + "days.mon": "Mon", + "days.sat": "Sat", + "days.sun": "Sun", + "days.thu": "Thu", + "days.tue": "Tue", + "days.wed": "Wed", + + "delete": "Delete", + "dimensions": "Dimensions", + "disabled": "Disabled", + "discard": "Discard", + "download": "Download", + "duplicate": "Duplicate", + "edit": "Edit", + + "dialog.files.empty": "No files to select", + "dialog.pages.empty": "No pages to select", + "dialog.users.empty": "No users to select", + + "email": "Email", + "email.placeholder": "mail@example.com", + + "error.access.login": "Invalid login", + "error.access.panel": "You are not allowed to access the panel", + "error.access.view": "You are not allowed to access this part of the panel", + + "error.avatar.create.fail": "The profile picture could not be uploaded", + "error.avatar.delete.fail": "The profile picture could not be deleted", + "error.avatar.dimensions.invalid": + "Please keep the width and height of the profile picture under 3000 pixels", + "error.avatar.mime.forbidden": + "The profile picture must be JPEG or PNG files", + + "error.blueprint.notFound": "The blueprint \"{name}\" could not be loaded", + + "error.email.preset.notFound": "The email preset \"{name}\" cannot be found", + + "error.field.converter.invalid": "Invalid converter \"{converter}\"", + + "error.file.changeName.empty": "The name must not be empty", + "error.file.changeName.permission": + "You are not allowed to change the name of \"{filename}\"", + "error.file.duplicate": "A file with the name \"{filename}\" already exists", + "error.file.extension.forbidden": + "The extension \"{extension}\" is not allowed", + "error.file.extension.missing": + "The extensions for \"{filename}\" is missing", + "error.file.maxheight": "The height of the image must not exceed {height} pixels", + "error.file.maxsize": "The file is too large", + "error.file.maxwidth": "The width of the image must not exceed {width} pixels", + "error.file.mime.differs": + "The uploaded file must be of the same mime type \"{mime}\"", + "error.file.mime.forbidden": "The media type \"{mime}\" is not allowed", + "error.file.mime.invalid": "Invalid mime type: {mime}", + "error.file.mime.missing": + "The media type for \"{filename}\" cannot be detected", + "error.file.minheight": "The height of the image must be at least {height} pixels", + "error.file.minsize": "The file is too small", + "error.file.minwidth": "The width of the image must be at least {width} pixels", + "error.file.name.missing": "The filename must not be empty", + "error.file.notFound": "The file \"{filename}\" cannot be found", + "error.file.orientation": "The orientation of the image must be \"{orientation}\"", + "error.file.type.forbidden": "You are not allowed to upload {type} files", + "error.file.undefined": "The file cannot be found", + + "error.form.incomplete": "Please fix all form errors…", + "error.form.notSaved": "The form could not be saved", + + "error.language.code": "Please enter a valid code for the language", + "error.language.duplicate": "The language already exists", + "error.language.name": "Please enter a valid name for the language", + + "error.license.format": "Please enter a valid license key", + "error.license.email": "Please enter a valid email address", + "error.license.verification": "The license could not be verified", + + "error.page.changeSlug.permission": + "You are not allowed to change the URL appendix for \"{slug}\"", + "error.page.changeStatus.incomplete": + "The page has errors and cannot be published", + "error.page.changeStatus.permission": + "The status for this page cannot be changed", + "error.page.changeStatus.toDraft.invalid": + "The page \"{slug}\" cannot be converted to a draft", + "error.page.changeTemplate.invalid": + "The template for the page \"{slug}\" cannot be changed", + "error.page.changeTemplate.permission": + "You are not allowed to change the template for \"{slug}\"", + "error.page.changeTitle.empty": "The title must not be empty", + "error.page.changeTitle.permission": + "You are not allowed to change the title for \"{slug}\"", + "error.page.create.permission": "You are not allowed to create \"{slug}\"", + "error.page.delete": "The page \"{slug}\" cannot be deleted", + "error.page.delete.confirm": "Please enter the page title to confirm", + "error.page.delete.hasChildren": + "The page has subpages and cannot be deleted", + "error.page.delete.permission": "You are not allowed to delete \"{slug}\"", + "error.page.draft.duplicate": + "A page draft with the URL appendix \"{slug}\" already exists", + "error.page.duplicate": + "A page with the URL appendix \"{slug}\" already exists", + "error.page.duplicate.permission": "You are not allowed to duplicate \"{slug}\"", + "error.page.notFound": "The page \"{slug}\" cannot be found", + "error.page.num.invalid": + "Please enter a valid sorting number. Numbers must not be negative.", + "error.page.slug.invalid": "Please enter a valid URL prefix", + "error.page.sort.permission": "The page \"{slug}\" cannot be sorted", + "error.page.status.invalid": "Please set a valid page status", + "error.page.undefined": "The page cannot be found", + "error.page.update.permission": "You are not allowed to update \"{slug}\"", + + "error.section.files.max.plural": + "You must not add more than {max} files to the \"{section}\" section", + "error.section.files.max.singular": + "You must not add more than one file to the \"{section}\" section", + "error.section.files.min.plural": + "The \"{section}\" section requires at least {min} files", + "error.section.files.min.singular": + "The \"{section}\" section requires at least one file", + + "error.section.pages.max.plural": + "You must not add more than {max} pages to the \"{section}\" section", + "error.section.pages.max.singular": + "You must not add more than one page to the \"{section}\" section", + "error.section.pages.min.plural": + "The \"{section}\" section requires at least {min} pages", + "error.section.pages.min.singular": + "The \"{section}\" section requires at least one page", + + "error.section.notLoaded": "The section \"{name}\" could not be loaded", + "error.section.type.invalid": "The section type \"{type}\" is not valid", + + "error.site.changeTitle.empty": "The title must not be empty", + "error.site.changeTitle.permission": + "You are not allowed to change the title of the site", + "error.site.update.permission": "You are not allowed to update the site", + + "error.template.default.notFound": "The default template does not exist", + + "error.user.changeEmail.permission": + "You are not allowed to change the email for the user \"{name}\"", + "error.user.changeLanguage.permission": + "You are not allowed to change the language for the user \"{name}\"", + "error.user.changeName.permission": + "You are not allowed to change the name for the user \"{name}\"", + "error.user.changePassword.permission": + "You are not allowed to change the password for the user \"{name}\"", + "error.user.changeRole.lastAdmin": + "The role for the last admin cannot be changed", + "error.user.changeRole.permission": + "You are not allowed to change the role for the user \"{name}\"", + "error.user.changeRole.toAdmin": + "You are not allowed to promote someone to the admin role", + "error.user.create.permission": "You are not allowed to create this user", + "error.user.delete": "The user \"{name}\" cannot be deleted", + "error.user.delete.lastAdmin": "The last admin cannot be deleted", + "error.user.delete.lastUser": "The last user cannot be deleted", + "error.user.delete.permission": + "You are not allowed to delete the user \"{name}\"", + "error.user.duplicate": + "A user with the email address \"{email}\" already exists", + "error.user.email.invalid": "Please enter a valid email address", + "error.user.language.invalid": "Please enter a valid language", + "error.user.notFound": "The user \"{name}\" cannot be found", + "error.user.password.invalid": + "Please enter a valid password. Passwords must be at least 8 characters long.", + "error.user.password.notSame": "The passwords do not match", + "error.user.password.undefined": "The user does not have a password", + "error.user.role.invalid": "Please enter a valid role", + "error.user.update.permission": + "You are not allowed to update the user \"{name}\"", + + "error.validation.accepted": "Please confirm", + "error.validation.alpha": "Please only enter characters between a-z", + "error.validation.alphanum": + "Please only enter characters between a-z or numerals 0-9", + "error.validation.between": + "Please enter a value between \"{min}\" and \"{max}\"", + "error.validation.boolean": "Please confirm or deny", + "error.validation.contains": + "Please enter a value that contains \"{needle}\"", + "error.validation.date": "Please enter a valid date", + "error.validation.date.after": "Please enter a date after {date}", + "error.validation.date.before": "Please enter a date before {date}", + "error.validation.date.between": "Please enter a date between {min} and {max}", + "error.validation.denied": "Please deny", + "error.validation.different": "The value must not be \"{other}\"", + "error.validation.email": "Please enter a valid email address", + "error.validation.endswith": "The value must end with \"{end}\"", + "error.validation.filename": "Please enter a valid filename", + "error.validation.in": "Please enter one of the following: ({in})", + "error.validation.integer": "Please enter a valid integer", + "error.validation.ip": "Please enter a valid IP address", + "error.validation.less": "Please enter a value lower than {max}", + "error.validation.match": "The value does not match the expected pattern", + "error.validation.max": "Please enter a value equal to or lower than {max}", + "error.validation.maxlength": + "Please enter a shorter value. (max. {max} characters)", + "error.validation.maxwords": "Please enter no more than {max} word(s)", + "error.validation.min": "Please enter a value equal to or greater than {min}", + "error.validation.minlength": + "Please enter a longer value. (min. {min} characters)", + "error.validation.minwords": "Please enter at least {min} word(s)", + "error.validation.more": "Please enter a greater value than {min}", + "error.validation.notcontains": + "Please enter a value that does not contain \"{needle}\"", + "error.validation.notin": + "Please don't enter any of the following: ({notIn})", + "error.validation.option": "Please select a valid option", + "error.validation.num": "Please enter a valid number", + "error.validation.required": "Please enter something", + "error.validation.same": "Please enter \"{other}\"", + "error.validation.size": "The size of the value must be \"{size}\"", + "error.validation.startswith": "The value must start with \"{start}\"", + "error.validation.time": "Please enter a valid time", + "error.validation.url": "Please enter a valid URL", + + "field.required": "The field is required", + "field.files.empty": "No files selected yet", + "field.pages.empty": "No pages selected yet", + "field.structure.delete.confirm": "Do you really want to delete this row?", + "field.structure.empty": "No entries yet", + "field.users.empty": "No users selected yet", + + "file.delete.confirm": + "Do you really want to delete
{filename}?", + + "files": "Files", + "files.empty": "No files yet", + + "hour": "Hour", + "insert": "Insert", + "install": "Install", + + "installation": "Installation", + "installation.completed": "The panel has been installed", + "installation.disabled": "The panel installer is disabled on public servers by default. Please run the installer on a local machine or enable it with the panel.install option.", + "installation.issues.accounts": + "The /site/accounts folder does not exist or is not writable", + "installation.issues.content": + "The /content folder does not exist or is not writable", + "installation.issues.curl": "The CURL extension is required", + "installation.issues.headline": "The panel cannot be installed", + "installation.issues.mbstring": + "The MB String extension is required", + "installation.issues.media": + "The /media folder does not exist or is not writable", + "installation.issues.php": "Make sure to use PHP 7+", + "installation.issues.server": + "Kirby requires Apache, Nginx or Caddy", + "installation.issues.sessions": "The /site/sessions folder does not exist or is not writable", + + "language": "Language", + "language.code": "Code", + "language.convert": "Make default", + "language.convert.confirm": + "

Do you really want to convert {name} to the default language? This cannot be undone.

If {name} has untranslated content, there will no longer be a valid fallback and parts of your site might be empty.

", + "language.create": "Add a new language", + "language.delete.confirm": + "Do you really want to delete the language {name} including all translations? This cannot be undone!", + "language.deleted": "The language has been deleted", + "language.direction": "Reading direction", + "language.direction.ltr": "Left to right", + "language.direction.rtl": "Right to left", + "language.locale": "PHP locale string", + "language.locale.warning": "You are using a custom locale set up. Please modify it in the language file in /site/languages", + "language.name": "Name", + "language.updated": "The language has been updated", + + "languages": "Languages", + "languages.default": "Default language", + "languages.empty": "There are no languages yet", + "languages.secondary": "Secondary languages", + "languages.secondary.empty": "There are no secondary languages yet", + + "license": "License", + "license.buy": "Buy a license", + "license.register": "Register", + "license.register.help": + "You received your license code after the purchase via email. Please copy and paste it to register.", + "license.register.label": "Please enter your license code", + "license.register.success": "Thank you for supporting Kirby", + "license.unregistered": "This is an unregistered demo of Kirby", + + "link": "Link", + "link.text": "Link text", + + "loading": "Loading", + + "lock.unsaved": "Unsaved changes", + "lock.unsaved.empty": "There are no more unsaved changes", + "lock.isLocked": "Unsaved changes by {email}", + "lock.file.isLocked": "The file is currently being edited by {email} and cannot be changed.", + "lock.page.isLocked": "The page is currently being edited by {email} and cannot be changed.", + "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.remember": "Keep me logged in", + + "logout": "Logout", + + "menu": "Menu", + "meridiem": "AM/PM", + "mime": "Media Type", + "minutes": "Minutes", + + "month": "Month", + "months.april": "April", + "months.august": "August", + "months.december": "December", + "months.february": "Feburary", + "months.january": "January", + "months.july": "July", + "months.june": "June", + "months.march": "March", + "months.may": "May", + "months.november": "November", + "months.october": "October", + "months.september": "September", + + "more": "More", + "name": "Name", + "next": "Next", + "off": "off", + "on": "on", + "open": "Open", + "options": "Options", + + "orientation": "Orientation", + "orientation.landscape": "Landscape", + "orientation.portrait": "Portrait", + "orientation.square": "Square", + + "page.changeSlug": "Change URL", + "page.changeSlug.fromTitle": "Create from title", + "page.changeStatus": "Change status", + "page.changeStatus.position": "Please select a position", + "page.changeStatus.select": "Select a new status", + "page.changeTemplate": "Change template", + "page.delete.confirm": + "Do you really want to delete {title}?", + "page.delete.confirm.subpages": + "This page has subpages.
All subpages will be deleted as well.", + "page.delete.confirm.title": "Enter the page title to confirm", + "page.draft.create": "Create draft", + "page.duplicate.appendix": "Copy", + "page.duplicate.files": "Copy files", + "page.duplicate.pages": "Copy pages", + "page.status": "Status", + "page.status.draft": "Draft", + "page.status.draft.description": + "The page is in draft mode and only visible for logged in editors", + "page.status.listed": "Public", + "page.status.listed.description": "The page is public for anyone", + "page.status.unlisted": "Unlisted", + "page.status.unlisted.description": "The page is only accessible via URL", + + "pages": "Pages", + "pages.empty": "No pages yet", + "pages.status.draft": "Drafts", + "pages.status.listed": "Published", + "pages.status.unlisted": "Unlisted", + + "pagination.page": "Page", + + "password": "Password", + "pixel": "Pixel", + "prev": "Previous", + "remove": "Remove", + "rename": "Rename", + "replace": "Replace", + "retry": "Try again", + "revert": "Revert", + + "role": "Role", + "role.admin.description": "The admin has all rights", + "role.admin.title": "Admin", + "role.all": "All", + "role.empty": "There are no users with this role", + "role.description.placeholder": "No description", + "role.nobody.description": "This is a fallback role without any permissions", + "role.nobody.title": "Nobody", + + "save": "Save", + "search": "Search", + + "section.required": "The section is required", + + "select": "Select", + "settings": "Settings", + "size": "Size", + "slug": "URL appendix", + "sort": "Sort", + "title": "Title", + "template": "Template", + "today": "Today", + + "toolbar.button.code": "Code", + "toolbar.button.bold": "Bold", + "toolbar.button.email": "Email", + "toolbar.button.headings": "Headings", + "toolbar.button.heading.1": "Heading 1", + "toolbar.button.heading.2": "Heading 2", + "toolbar.button.heading.3": "Heading 3", + "toolbar.button.italic": "Italic", + "toolbar.button.file": "File", + "toolbar.button.file.select": "Select a file", + "toolbar.button.file.upload": "Upload a file", + "toolbar.button.link": "Link", + "toolbar.button.ol": "Ordered list", + "toolbar.button.ul": "Bullet list", + + "translation.author": "Kirby Team", + "translation.direction": "ltr", + "translation.name": "English", + "translation.locale": "en_US", + + "upload": "Upload", + "upload.error.cantMove": "The uploaded file could not be moved", + "upload.error.cantWrite": "Failed to write file to disk", + "upload.error.default": "The file could not be uploaded", + "upload.error.extension": "File upload stopped by extension", + "upload.error.formSize": "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the form", + "upload.error.iniPostSize": "The uploaded file exceeds the post_max_size directive in php.ini", + "upload.error.iniSize": "The uploaded file exceeds the upload_max_filesize directive in php.ini", + "upload.error.noFile": "No file was uploaded", + "upload.error.noFiles": "No files were uploaded", + "upload.error.partial": "The uploaded file was only partially uploaded", + "upload.error.tmpDir": "Missing a temporary folder", + "upload.errors": "Error", + "upload.progress": "Uploading…", + + "url": "Url", + "url.placeholder": "https://example.com", + + "user": "User", + "user.blueprint": + "You can define additional sections and form fields for this user role in /site/blueprints/users/{role}.yml", + "user.changeEmail": "Change email", + "user.changeLanguage": "Change language", + "user.changeName": "Rename this user", + "user.changePassword": "Change password", + "user.changePassword.new": "New password", + "user.changePassword.new.confirm": "Confirm the new password…", + "user.changeRole": "Change role", + "user.changeRole.select": "Select a new role", + "user.create": "Add a new user", + "user.delete": "Delete this user", + "user.delete.confirm": + "Do you really want to delete
{email}?", + + "users": "Users", + + "version": "Version", + + "view.account": "Your account", + "view.installation": "Installation", + "view.settings": "Settings", + "view.site": "Site", + "view.users": "Users", + + "welcome": "Welcome", + "year": "Year" +} diff --git a/kirby/i18n/translations/es_419.json b/kirby/i18n/translations/es_419.json new file mode 100755 index 0000000..0d476b7 --- /dev/null +++ b/kirby/i18n/translations/es_419.json @@ -0,0 +1,481 @@ +{ + "add": "Agregar", + "avatar": "Foto de perfil", + "back": "Regresar", + "cancel": "Cancelar", + "change": "Cambiar", + "close": "Cerrar", + "confirm": "De acuerdo", + "copy": "Copiar", + "create": "Crear", + + "date": "Fecha", + "date.select": "Selecciona una fecha", + + "day": "Día", + "days.fri": "Vie", + "days.mon": "Lun", + "days.sat": "S\u00e1b", + "days.sun": "Dom", + "days.thu": "Jue", + "days.tue": "Mar", + "days.wed": "Mi\u00e9", + + "delete": "Eliminar", + "dimensions": "Dimensiones", + "disabled": "Desabilitado", + "discard": "Descartar", + "download": "Descargar", + "duplicate": "Duplicar", + "edit": "Editar", + + "dialog.files.empty": "No has seleccionado ningún archivo", + "dialog.pages.empty": "No has seleccionado ninguna página", + "dialog.users.empty": "No has seleccionado ningún usuario", + + "email": "Correo Electrónico", + "email.placeholder": "correo@ejemplo.com", + + "error.access.login": "Ingreso inválido", + "error.access.panel": "No tienes permitido acceder al panel.", + "error.access.view": "No tienes permiso para acceder a esta parte del panel", + + "error.avatar.create.fail": "No se pudo subir la foto de perfil.", + "error.avatar.delete.fail": "No se pudo eliminar la foto de perfil.", + "error.avatar.dimensions.invalid": + "Por favor, mantén el ancho y la altura de la imagen de perfil por debajo de 3000 pixeles.", + "error.avatar.mime.forbidden": + "La foto de perfil debe de ser un archivo JPG o PNG.", + + "error.blueprint.notFound": "El blueprint \"{name}\" no se pudo cargar.", + + "error.email.preset.notFound": "El preajuste de email \"{name}\" no se pudo encontrar.", + + "error.field.converter.invalid": "Convertidor inválido \"{converter}\"", + + "error.file.changeName.empty": "El nombre no debe estar vacío", + "error.file.changeName.permission": + "No tienes permitido cambiar el nombre de \"{filename}\"", + "error.file.duplicate": "Ya existe un archivo con el nombre \"{filename}\".", + "error.file.extension.forbidden": + "La extensión \"{extension}\" no está permitida.", + "error.file.extension.missing": + "Falta la extensión para \"{filename}\".", + "error.file.maxheight": "La altura de la imagen no debe exceder {height} pixeles", + "error.file.maxsize": "El archivo es muy grande", + "error.file.maxwidth": "El ancho de la imagen no debe exceder {width} pixeles", + "error.file.mime.differs": + "El archivo cargado debe ser del mismo tipo mime \"{mime}\".", + "error.file.mime.forbidden": "El tipo de medios \"{mime}\" no está permitido.", + "error.file.mime.invalid": "Tipo invalido de mime: {mime}", + "error.file.mime.missing": + "No se puede detectar el tipo de medio para \"{filename}\".", + "error.file.minheight": "La altura de la imagen debe ser de al menos {height} pixeles", + "error.file.minsize": "El archivo es muy pequeño", + "error.file.minwidth": "El ancho de la imagen debe ser de al menos {width} pixeles", + "error.file.name.missing": "El nombre del archivo no debe estar vacío.", + "error.file.notFound": "El archivo \"{filename}\" no pudo ser encontrado.", + "error.file.orientation": "La orientación de la imagen debe ser \"{orientation}\"", + "error.file.type.forbidden": "No está permitido subir archivos {type}.", + "error.file.undefined": "El archivo no se puede encontrar.", + + "error.form.incomplete": "Por favor, corrige todos los errores del formulario...", + "error.form.notSaved": "No se pudo guardar el formulario.", + + "error.language.code": "Por favor introduce un código válido para el lenguaje", + "error.language.duplicate": "El lenguaje ya existe", + "error.language.name": "Por favor introduce un nombre válido para el lenguaje", + + "error.license.format": "Por favor introduce una llave de licencia válida", + "error.license.email": "Por favor ingresa un correo electrónico valido", + "error.license.verification": "La licencia no pude ser verificada", + + "error.page.changeSlug.permission": + "No está permitido cambiar el apéndice de URL para \"{slug}\".", + "error.page.changeStatus.incomplete": + "La página tiene errores y no puede ser publicada.", + "error.page.changeStatus.permission": + "El estado de esta página no se puede cambiar.", + "error.page.changeStatus.toDraft.invalid": + "La página \"{slug}\" no se puede convertir en un borrador", + "error.page.changeTemplate.invalid": + "La plantilla para la página \"{slug}\" no se puede cambiar", + "error.page.changeTemplate.permission": + "No está permitido cambiar la plantilla para \"{slug}\"", + "error.page.changeTitle.empty": "El título no debe estar vacío.", + "error.page.changeTitle.permission": + "No tienes permiso para cambiar el título de \"{slug}\"", + "error.page.create.permission": "No tienes permiso para crear \"{slug}\"", + "error.page.delete": "La página \"{slug}\" no se puede eliminar", + "error.page.delete.confirm": "Por favor, introduce el título de la página para confirmar", + "error.page.delete.hasChildren": + "La página tiene subpáginas y no se puede eliminar", + "error.page.delete.permission": "No tienes permiso para borrar \"{slug}\"", + "error.page.draft.duplicate": + "Ya existe un borrador de página con el apéndice de URL \"{slug}\"", + "error.page.duplicate": + "Ya existe una página con el apéndice de URL \"{slug}\"", + "error.page.duplicate.permission": "No tienes permitido duplicar \"{slug}\"", + "error.page.notFound": "La página \"{slug}\" no se encuentra", + "error.page.num.invalid": + "Por favor, introduce un número de posición válido. Los números no deben ser negativos.", + "error.page.slug.invalid": "Por favor ingresa un prefijo de URL válido", + "error.page.sort.permission": "La página \"{slug}\" no se puede ordenar", + "error.page.status.invalid": "Por favor, establece una estado de página válido", + "error.page.undefined": "La p\u00e1gina no fue encontrada", + "error.page.update.permission": "No tienes permiso para actualizar \"{slug}\"", + + "error.section.files.max.plural": + "No debes agregar más de {max} archivos a la sección \"{section}\"", + "error.section.files.max.singular": + "No debes agregar más de un archivo a la sección \"{section}\"", + "error.section.files.min.plural": + "La sección \"{section}\" requiere al menos {min} archivos", + "error.section.files.min.singular": + "La sección \"{section}\" requiere al menos un archivo", + + "error.section.pages.max.plural": + "No debes agregar más de {max} páginas a la sección \"{section}\"", + "error.section.pages.max.singular": + "No debes agregar más de una página a la sección \"{section}\"", + "error.section.pages.min.plural": + "La sección \"{section}\" requiere al menos {min} páginas", + "error.section.pages.min.singular": + "La sección \"{section}\" requiere al menos una página", + + "error.section.notLoaded": "La sección \"{name}\" no se pudo cargar", + "error.section.type.invalid": "La sección \"{type}\" no es valida", + + "error.site.changeTitle.empty": "El título no debe estar vacío.", + "error.site.changeTitle.permission": + "No tienes permiso para cambiar el título del sitio", + "error.site.update.permission": "No tienes permiso de actualizar el sitio", + + "error.template.default.notFound": "La plantilla predeterminada no existe", + + "error.user.changeEmail.permission": + "No tienes permiso para cambiar el email del usuario \"{name}\"", + "error.user.changeLanguage.permission": + "No tienes permiso para cambiar el idioma del usuario \"{name}\"", + "error.user.changeName.permission": + "No tienes permiso para cambiar el nombre del usuario \"{name}\"", + "error.user.changePassword.permission": + "No tienes permiso para cambiar la contraseña del usuario \"{name}\"", + "error.user.changeRole.lastAdmin": + "El rol del último administrador no puede ser cambiado", + "error.user.changeRole.permission": + "No tienes permiso para cambiar el rol del usuario \"{name}\"", + "error.user.changeRole.toAdmin": + "No tienes permitido promover a alguien al rol de admin", + "error.user.create.permission": "No tienes permiso de crear este usuario", + "error.user.delete": "El ususario no pudo ser eliminado", + "error.user.delete.lastAdmin": "Usted no puede borrar el \u00faltimo administrador", + "error.user.delete.lastUser": "El último usuario no puede ser borrado", + "error.user.delete.permission": + "Usted no tiene permitido borrar este usuario", + "error.user.duplicate": + "Ya existe un usuario con el email \"{email}\"", + "error.user.email.invalid": "Por favor ingresa un correo electrónico valido", + "error.user.language.invalid": "Por favor ingresa un idioma valido", + "error.user.notFound": "El usuario no pudo ser encontrado", + "error.user.password.invalid": + "Por favor ingresa una contraseña valida. Las contraseñas deben tener al menos 8 caracteres de largo.", + "error.user.password.notSame": "Por favor confirma la contrase\u00f1a", + "error.user.password.undefined": "El usuario no tiene contraseña", + "error.user.role.invalid": "Por favor ingresa un rol valido", + "error.user.update.permission": + "No tienes permiso para actualizar al usuario \"{name}\"", + + "error.validation.accepted": "Por favor, confirma", + "error.validation.alpha": "Por favor ingrese solo caracteres entre a-z", + "error.validation.alphanum": + "Por favor ingrese solo caracteres entre a-z o números entre 0-9", + "error.validation.between": + "Por favor ingrese valores entre \"{min}\" y \"{max}\"", + "error.validation.boolean": "Por favor confirme o niegue", + "error.validation.contains": + "Por favor ingrese valores que contengan \"{needle}\"", + "error.validation.date": "Por favor ingresa una fecha válida", + "error.validation.date.after": "Por favor introduce una fecha posterior a {date}", + "error.validation.date.before": "Por favor introduce una fecha anterior a {date}", + "error.validation.date.between": "Por favor introduce un número entre {min} y {max}", + "error.validation.denied": "Por favor niegue", + "error.validation.different": "EL valor no debe ser \"{other}\"", + "error.validation.email": "Por favor ingresa un correo electrónico valido", + "error.validation.endswith": "El valor no debe terminar con \"{end}\"", + "error.validation.filename": "Por favor ingresa un nombre de archivo válido", + "error.validation.in": "Por favor ingresa uno de los siguientes: ({in})", + "error.validation.integer": "Por favor ingresa un entero válido", + "error.validation.ip": "Por favor ingresa una dirección IP válida", + "error.validation.less": "Por favor ingresa un valor menor a {max}", + "error.validation.match": "El valor no coincide con el patrón esperado", + "error.validation.max": "Por favor ingresa un valor menor o igual a {max}", + "error.validation.maxlength": + "Por favor ingresa un valor mas corto. (max. {max} caracteres)", + "error.validation.maxwords": "Por favor ingresa no mas de {max} palabra(s)", + "error.validation.min": "Por favor ingresa un valor mayor o igual a {min}", + "error.validation.minlength": + "Por favor ingresa un valor mas largo. (min. {min} caracteres)", + "error.validation.minwords": "Por favor ingresa al menos {min} palabra(s)", + "error.validation.more": "Por favor ingresa un valor mayor a {min}", + "error.validation.notcontains": + "Por favor ingresa un valor que no contenga \"{needle}\"", + "error.validation.notin": + "Por favor no ingreses ninguno de las siguientes: ({notIn})", + "error.validation.option": "Por favor selecciona una de las opciones válidas", + "error.validation.num": "Por favor ingresa un numero válido", + "error.validation.required": "Por favor ingresa algo", + "error.validation.same": "Por favor ingresa \"{other}\"", + "error.validation.size": "El tamaño del valor debe ser \"{size}\"", + "error.validation.startswith": "El valor debe comenzar con \"{start}\"", + "error.validation.time": "Por favor ingresa una hora válida", + "error.validation.url": "Por favor ingresa un URL válido", + + "field.required": "Este campo es requerido", + "field.files.empty": "Aún no ha seleccionado ningún archivo", + "field.pages.empty": "Aún no ha seleccionado ningúna pagina", + "field.structure.delete.confirm": "\u00bfEn realidad desea borrar esta entrada?", + "field.structure.empty": "A\u00fan no existen entradas.", + "field.users.empty": "Aún no ha seleccionado ningún usuario", + + "file.delete.confirm": + "\u00bfEst\u00e1s seguro que deseas eliminar este archivo?", + + "files": "Archivos", + "files.empty": "Aún no existen archivos", + + "hour": "Hora", + "insert": "Insertar", + "install": "Instalar", + + "installation": "Instalación", + "installation.completed": "El panel ha sido instalado.", + "installation.disabled": "El instalador del panel está deshabilitado en servidores públicos por defecto. Ejecute el instalador en una máquina local o habilítelo con la opción panel.install.", + "installation.issues.accounts": + "La carpeta /site/accounts no existe o no posee permisos de escritura.", + "installation.issues.content": + "La carpeta /content no existe o no posee permisos de escritura.", + "installation.issues.curl": "Se requiere la extensión CURL.", + "installation.issues.headline": "El panel no puede ser instalado.", + "installation.issues.mbstring": + "Se requiere la extensión MB String.", + "installation.issues.media": + "La carpeta /media no existe o no posee permisos de escritura.", + "installation.issues.php": "Asegurese de estar usando PHP 7+", + "installation.issues.server": + "Kirby requiere Apache, Nginx, Caddy", + "installation.issues.sessions": "La carpeta /site/sessions no existe o no posee permisos de escritura.", + + "language": "Idioma", + "language.code": "Código", + "language.convert": "Hacer por defecto", + "language.convert.confirm": + "

Realmente deseas convertir {name} al idioma por defecto? Esta acción no se puede deshacer.

Si {name} tiene contenido sin traducir, no habrá vuelta atras y tu sitio puede quedar con partes sin contenido.

", + "language.create": "Añadir nuevo idioma", + "language.delete.confirm": + "

", + "language.deleted": "El idioma ha sido borrado", + "language.direction": "Dirección de lectura", + "language.direction.ltr": "De Izquierda a derecha", + "language.direction.rtl": "De derecha a izquierda", + "language.locale": "Cadena de localización PHP", + "language.locale.warning": "Estas utilizando un configuración local. Por favor modifícalo en el archivo del lenguaje en /site/languages", + "language.name": "Nombre", + "language.updated": "El idioma a sido actualizado", + + "languages": "Idiomas", + "languages.default": "Idioma por defecto", + "languages.empty": "Todavía no hay idiomas", + "languages.secondary": "Idiomas secundarios", + "languages.secondary.empty": "Todavía no hay idiomas secundarios", + + "license": "Licencia", + "license.buy": "Comprar una licencia", + "license.register": "Registrar", + "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", + + "link": "Enlace", + "link.text": "Texto de Enlace", + + "loading": "Cargando", + + "lock.unsaved": "Cambios sin guardar", + "lock.unsaved.empty": "No hay más cambios sin guardar", + "lock.isLocked": "Cambios sin guardar por {email}", + "lock.file.isLocked": "El archivo está siendo actualmente editado por {email} y no puede ser cambiado.", + "lock.page.isLocked": "La página está siendo actualmente editada por {email} y no puede ser cambiada.", + "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.remember": "Mantener mi sesión iniciada", + + "logout": "Cerrar sesi\u00f3n", + + "menu": "Menù", + "meridiem": "AM/PM", + "mime": "Tipos de medios", + "minutes": "Minutos", + + "month": "Mes", + "months.april": "Abril", + "months.august": "Agosto", + "months.december": "Diciembre", + "months.february": "Febrero", + "months.january": "Enero", + "months.july": "Julio", + "months.june": "Junio", + "months.march": "Marzo", + "months.may": "Mayo", + "months.november": "Noviembre", + "months.october": "Octubre", + "months.september": "Septiembre", + + "more": "Màs", + "name": "Nombre", + "next": "Siguiente", + "off": "Apagado", + "on": "Encendido", + "open": "Abrir", + "options": "Opciones", + + "orientation": "Orientación", + "orientation.landscape": "Paisaje", + "orientation.portrait": "Retrato", + "orientation.square": "Diapositiva", + + "page.changeSlug": "Cambiar URL", + "page.changeSlug.fromTitle": "Crear a partir del t\u00edtulo", + "page.changeStatus": "Cambiar estado", + "page.changeStatus.position": "Por favor selecciona una posición", + "page.changeStatus.select": "Selecciona un nuevo estado", + "page.changeTemplate": "Cambiar plantilla", + "page.delete.confirm": + "¿Estás seguro que deseas eliminar {title}?", + "page.delete.confirm.subpages": + "Esta página tiene subpáginas.
Todas las súbpaginas serán eliminadas también.", + "page.delete.confirm.title": "Introduce el título de la página para confirmar", + "page.draft.create": "Crear borrador", + "page.duplicate.appendix": "Copiar", + "page.duplicate.files": "Copiar archivos", + "page.duplicate.pages": "Copiar páginas", + "page.status": "Estado", + "page.status.draft": "Borrador", + "page.status.draft.description": + "La página está en modo de borrador y sólo es visible para los editores registrados", + "page.status.listed": "Pública", + "page.status.listed.description": "La página es pública para cualquiera", + "page.status.unlisted": "No publicada", + "page.status.unlisted.description": "La página sólo es accesible vía URL", + + "pages": "Páginas", + "pages.empty": "No hay páginas aún", + "pages.status.draft": "Borradores", + "pages.status.listed": "Publicado", + "pages.status.unlisted": "No publicado", + + "pagination.page": "Página", + + "password": "Contrase\u00f1a", + "pixel": "Pixel", + "prev": "Anterior", + "remove": "Eliminar", + "rename": "Renombrar", + "replace": "Reemplazar", + "retry": "Reintentar", + "revert": "Revertir", + + "role": "Rol", + "role.admin.description": "El administrador tiene todos los derechos", + "role.admin.title": "Administrador", + "role.all": "Todos", + "role.empty": "No hay usuarios con este rol", + "role.description.placeholder": "Sin descripción", + "role.nobody.description": "Este es un rol alternativo sin permisos", + "role.nobody.title": "Nadie", + + "save": "Guardar", + "search": "Buscar", + + "section.required": "Esta sección es requerida", + + "select": "Seleccionar", + "settings": "Ajustes", + "size": "Tamaño", + "slug": "Apéndice URL", + "sort": "Ordenar", + "title": "Título", + "template": "Plantilla", + "today": "Hoy", + + "toolbar.button.code": "Código", + "toolbar.button.bold": "Negrita", + "toolbar.button.email": "Email", + "toolbar.button.headings": "Encabezados", + "toolbar.button.heading.1": "Encabezado 1", + "toolbar.button.heading.2": "Encabezado 2", + "toolbar.button.heading.3": "Encabezado 3", + "toolbar.button.italic": "Texto en It\u00e1licas", + "toolbar.button.file": "Archivo", + "toolbar.button.file.select": "Selecciona un archivo", + "toolbar.button.file.upload": "Sube un archivo", + "toolbar.button.link": "Enlace", + "toolbar.button.ol": "Lista en orden", + "toolbar.button.ul": "Lista de viñetas", + + "translation.author": "Equipo Kirby", + "translation.direction": "ltr", + "translation.name": "Español (América Latina)", + "translation.locale": "es_419", + + "upload": "Subir", + "upload.error.cantMove": "El archivo subido no puede ser movido", + "upload.error.cantWrite": "Error al escribir el archivo en el disco", + "upload.error.default": "El archivo no pudo ser subido", + "upload.error.extension": "Subida de archivo detenida por la extensión", + "upload.error.formSize": "El archivo subido excede la directiva MAX_FILE_SIZE que fue especificada en el formulario", + "upload.error.iniPostSize": "El archivo subido excede la directiva post_max_size directive en php.ini", + "upload.error.iniSize": "El archivo subido excede la directiva upload_max_filesize en php.ini", + "upload.error.noFile": "Ningún archivo ha sido subido", + "upload.error.noFiles": "Ningún archivo ha sido subido", + "upload.error.partial": "El archivo ha sido subido solo parcialmente", + "upload.error.tmpDir": "No se encuentra la carpeta temporal", + "upload.errors": "Error", + "upload.progress": "Subiendo...", + + "url": "Url", + "url.placeholder": "https://ejemplo.com", + + "user": "Usuario", + "user.blueprint": + "Puedes definir secciones adicionales y campos de formulario para este rol de usuario en /site/blueprints/users/{role}.yml", + "user.changeEmail": "Cambiar correo electrónico", + "user.changeLanguage": "Cambiar idioma", + "user.changeName": "Renombrar este usuario", + "user.changePassword": "Cambiar la contraseña", + "user.changePassword.new": "Nueva contraseña", + "user.changePassword.new.confirm": "Confirma la nueva contraseña...", + "user.changeRole": "Cambiar rol", + "user.changeRole.select": "Selecciona un nuevo rol", + "user.create": "Agregar un nuevo usuario", + "user.delete": "Eliminar este usuario", + "user.delete.confirm": + "¿Estás seguro que deseas eliminar
{email}?", + + "users": "Usuarios", + + "version": "Versión", + + "view.account": "Tu cuenta", + "view.installation": "Instalaci\u00f3n", + "view.settings": "Ajustes", + "view.site": "Sitio", + "view.users": "Usuarios", + + "welcome": "Bienvenido", + "year": "Año" +} diff --git a/kirby/i18n/translations/es_ES.json b/kirby/i18n/translations/es_ES.json new file mode 100755 index 0000000..f73a36a --- /dev/null +++ b/kirby/i18n/translations/es_ES.json @@ -0,0 +1,481 @@ +{ + "add": "Añadir", + "avatar": "Foto de perfil", + "back": "Atrás", + "cancel": "Cancelar", + "change": "Cambiar", + "close": "Cerrar", + "confirm": "Confirmar", + "copy": "Copiar", + "create": "Crear", + + "date": "Fecha", + "date.select": "Selecciona una fecha", + + "day": "Día", + "days.fri": "Vi", + "days.mon": "Lu", + "days.sat": "Sá", + "days.sun": "Do", + "days.thu": "Ju", + "days.tue": "Ma", + "days.wed": "Mi", + + "delete": "Borrar", + "dimensions": "Dimensiones", + "disabled": "Desabilitado", + "discard": "Descartar", + "download": "Descargar", + "duplicate": "Duplicar", + "edit": "Editar", + + "dialog.files.empty": "No se ha seleccionado ningún archivo", + "dialog.pages.empty": "No se ha seleccionado ninguna página", + "dialog.users.empty": "No se ha seleccionado ningún usuario", + + "email": "Correo electrónico", + "email.placeholder": "correo@ejemplo.com", + + "error.access.login": "Ingreso inválido", + "error.access.panel": "No estás autorizado para acceder al panel", + "error.access.view": "You are not allowed to access this part of the panel", + + "error.avatar.create.fail": "No se pudo subir la foto de perfil.", + "error.avatar.delete.fail": "No se pudo borrar la foto de perfil", + "error.avatar.dimensions.invalid": + "Por favor, mantenga el ancho y la altura de la imagen de perfil debajo de 3000 píxeles", + "error.avatar.mime.forbidden": + "La imagen del perfil debe ser JPEG o PNG.", + + "error.blueprint.notFound": "El blueprint \"{name}\" no pudo ser cargado", + + "error.email.preset.notFound": "El preset del correo \"{name}\" no pudo ser encontrado", + + "error.field.converter.invalid": "Convertidor \"{converter}\" inválido", + + "error.file.changeName.empty": "The name must not be empty", + "error.file.changeName.permission": + "No tienes permitido cambiar el nombre de \"{filename}\"", + "error.file.duplicate": "Ya existe un archivo con el nombre \"{filename}\"", + "error.file.extension.forbidden": + "La extensión \"{extension}\" no está permitida", + "error.file.extension.missing": + "Falta la extensión para \"{filename}\"", + "error.file.maxheight": "The height of the image must not exceed {height} pixels", + "error.file.maxsize": "The file is too large", + "error.file.maxwidth": "The width of the image must not exceed {width} pixels", + "error.file.mime.differs": + "El archivo cargado debe ser del mismo tipo mime \"{mime}\"", + "error.file.mime.forbidden": "Los medios tipo \"{mime}\" no están permitidos", + "error.file.mime.invalid": "Invalid mime type: {mime}", + "error.file.mime.missing": + "El tipo de medio para \"{filename}\" no pudo ser detectado", + "error.file.minheight": "The height of the image must be at least {height} pixels", + "error.file.minsize": "The file is too small", + "error.file.minwidth": "The width of the image must be at least {width} pixels", + "error.file.name.missing": "El nombre de archivo no debe estar vacío", + "error.file.notFound": "El archivo \"{filename}\" no pudo ser encontrado", + "error.file.orientation": "The orientation of the image must be \"{orientation}\"", + "error.file.type.forbidden": "No está permitido subir archivos {type}", + "error.file.undefined": "El archivo no pudo ser encontrado", + + "error.form.incomplete": "Por favor, corrija todos los errores del formulario…", + "error.form.notSaved": "El formulario no pudo ser guardado", + + "error.language.code": "Please enter a valid code for the language", + "error.language.duplicate": "The language already exists", + "error.language.name": "Please enter a valid name for the language", + + "error.license.format": "Please enter a valid license key", + "error.license.email": "Por favor, introduce un correo electrónico válido", + "error.license.verification": "The license could not be verified", + + "error.page.changeSlug.permission": + "No está permitido cambiar el apéndice de URL para \"{slug}\"", + "error.page.changeStatus.incomplete": + "La página tiene errores y no puede ser publicada.", + "error.page.changeStatus.permission": + "El estado de esta página no se puede cambiar", + "error.page.changeStatus.toDraft.invalid": + "La página \"{slug}\" no se puede convertir a borrador", + "error.page.changeTemplate.invalid": + "La plantilla para la página \"{slug}\" no se puede cambiar", + "error.page.changeTemplate.permission": + "No tienes permitido cambiar la plantilla para \"{slug}\"", + "error.page.changeTitle.empty": "El título no debe estar vacío.", + "error.page.changeTitle.permission": + "No tienes permitido cambiar el título por \"{slug}\"", + "error.page.create.permission": "No tienes permitido crear \"{slug}\"", + "error.page.delete": "La página \"{slug}\" no puede ser eliminada", + "error.page.delete.confirm": "Por favor, introduzca el título de la página para confirmar", + "error.page.delete.hasChildren": + "La página tiene subpáginas y no se puede eliminar", + "error.page.delete.permission": "No tienes permiso de eliminar \"{slug}\"", + "error.page.draft.duplicate": + "Un borrador de página con el apéndice de URL \"{slug}\" ya existe", + "error.page.duplicate": + "Una página con el apéndice de URL. \"{slug}\" ya existe", + "error.page.duplicate.permission": "You are not allowed to duplicate \"{slug}\"", + "error.page.notFound": "La página \"{slug}\" no puede ser encontrada", + "error.page.num.invalid": + "Por favor, introduzca un número válido. Estos no deben ser negativos.", + "error.page.slug.invalid": "Por favor ingrese un prefijo de URL válido", + "error.page.sort.permission": "La página \"{slug}\" no se puede ordenar", + "error.page.status.invalid": "Por favor, establezca un estado de página válido", + "error.page.undefined": "La página no se puede encontrar", + "error.page.update.permission": "No tienes permitido actualizar \"{slug}\"", + + "error.section.files.max.plural": + "No debes agregar más de {max} archivos a la sección \"{section}\"", + "error.section.files.max.singular": + "No debes agregar más de 1 archivo a la sección \"{section}\"", + "error.section.files.min.plural": + "La sección \"{section}\" requiere al menos {min} archivos", + "error.section.files.min.singular": + "La sección \"{section}\" requiere al menos un archivo", + + "error.section.pages.max.plural": + "No debe agregar más de {max} páginas a la sección \"{section}\"", + "error.section.pages.max.singular": + "No debe agregar más de una página a la sección \"{section}\"", + "error.section.pages.min.plural": + "La sección \"{section}\" requiere al menos {min} páginas", + "error.section.pages.min.singular": + "La sección \"{section}\" requiere al menos una página", + + "error.section.notLoaded": "La sección \"{name}\" no pudo ser cargada", + "error.section.type.invalid": "El sección tipo \"{tipo}\" no es válido", + + "error.site.changeTitle.empty": "El título no debe estar vacío.", + "error.site.changeTitle.permission": + "No está permitido cambiar el título del sitio", + "error.site.update.permission": "No tienes permitido actualizar el sitio", + + "error.template.default.notFound": "La plantilla por defecto no existe", + + "error.user.changeEmail.permission": + "No tienes permitido cambiar el correo electrónico para el usuario \"{name}\"", + "error.user.changeLanguage.permission": + "No tienes permitido cambiar el idioma para el usuario \"{name}\"", + "error.user.changeName.permission": + "No tienes permitido cambiar el nombre del usuario \"{name}\"", + "error.user.changePassword.permission": + "No tienes permitido cambiar la contraseña del usuario \"{name}\"", + "error.user.changeRole.lastAdmin": + "El rol para el último administrador no puede ser cambiado", + "error.user.changeRole.permission": + "No tienes permitido cambiar el rol del usuario \"{name}\"", + "error.user.changeRole.toAdmin": + "You are not allowed to promote someone to the admin role", + "error.user.create.permission": "No tienes permiso para crear este usuario", + "error.user.delete": "El usuario \"{name}\" no puede ser eliminado", + "error.user.delete.lastAdmin": "El último administrador no puede ser eliminado", + "error.user.delete.lastUser": "El último usuario no puede ser eliminado", + "error.user.delete.permission": + "No tienes permitido eliminar el usuario \"{name}\"", + "error.user.duplicate": + "Un usuario con la dirección de correo electrónico \"{email}\" ya existe", + "error.user.email.invalid": "Por favor, introduce una dirección de correo electrónico válida", + "error.user.language.invalid": "Por favor ingrese un idioma válido", + "error.user.notFound": "El usuario \"{name}\" no pudo ser encontrado", + "error.user.password.invalid": + "Por favor introduce una contraseña válida. Las contraseñas deben tener al menos 8 caracteres de largo.", + "error.user.password.notSame": "Las contraseñas no coinciden", + "error.user.password.undefined": "El usuario no tiene contraseña", + "error.user.role.invalid": "Por favor ingrese un rol válido", + "error.user.update.permission": + "No tienes permitido actualizar al usuario \"{name}\"", + + "error.validation.accepted": "Por favor, confirma", + "error.validation.alpha": "Por favor solo ingresa caracteres entre a-z", + "error.validation.alphanum": + "Por favor solo ingrese caracteres entre a-z o numerales 0-9", + "error.validation.between": + "Por favor, introduzca un valor entre \"{min}\" y \"{max}\"", + "error.validation.boolean": "Por favor confirme o rechace", + "error.validation.contains": + "Por favor ingrese un valor que contenga \"{needle}\"", + "error.validation.date": "Por favor introduzca una fecha valida", + "error.validation.date.after": "Please enter a date after {date}", + "error.validation.date.before": "Please enter a date before {date}", + "error.validation.date.between": "Please enter a date between {min} and {max}", + "error.validation.denied": "Por favor, rechace", + "error.validation.different": "El valor no debe ser \"{other}\"", + "error.validation.email": "Por favor, introduce un correo electrónico válido", + "error.validation.endswith": "El valor debe terminar con \"{end}\"", + "error.validation.filename": "Por favor ingrese un nombre de archivo válido", + "error.validation.in": "Por favor ingrese uno de los siguientes: ({in})", + "error.validation.integer": "Por favor, introduce un numero integro válido", + "error.validation.ip": "Por favor ingrese una dirección IP válida", + "error.validation.less": "Por favor, introduzca un valor inferior a {max}", + "error.validation.match": "El valor no coincide con el patrón esperado", + "error.validation.max": "Por favor, introduzca un valor igual o inferior a {max}", + "error.validation.maxlength": + "Por favor, introduzca un valor más corto. (max. {max} caracteres)", + "error.validation.maxwords": "Por favor ingrese no más de {max} palabra(s)", + "error.validation.min": "Por favor, introduzca un valor igual o mayor a {min}", + "error.validation.minlength": + "Por favor, introduzca un valor más largo. (min. {min} caracteres)", + "error.validation.minwords": "Por favor ingrese al menos {min} palabra(s)", + "error.validation.more": "Por favor, introduzca un valor mayor a {min}", + "error.validation.notcontains": + "Por favor ingrese un valor que no contenga \"{needle}\"", + "error.validation.notin": + "Por favor, no ingrese ninguno de los siguientes: ({notIn})", + "error.validation.option": "Por favor seleccione una opción válida", + "error.validation.num": "Por favor ingrese un número valido", + "error.validation.required": "Por favor ingrese algo", + "error.validation.same": "Por favor escribe \"{other}\"", + "error.validation.size": "El tamaño del valor debe ser \"{size}\"", + "error.validation.startswith": "El valor debe comenzar con \"{start}\"", + "error.validation.time": "Por favor ingrese una hora válida", + "error.validation.url": "Por favor introduzca un URL válido", + + "field.required": "The field is required", + "field.files.empty": "Aún no hay archivos seleccionados", + "field.pages.empty": "Aún no hay páginas seleccionadas", + "field.structure.delete.confirm": "¿Realmente quieres eliminar esta fila?", + "field.structure.empty": "Aún no hay entradas", + "field.users.empty": "Aún no hay usuarios seleccionados", + + "file.delete.confirm": + "¿Realmente quieres eliminar
{filename}?", + + "files": "Archivos", + "files.empty": "Aún no hay archivos", + + "hour": "Hora", + "insert": "Insertar", + "install": "Instalar", + + "installation": "Instalación", + "installation.completed": "El panel ha sido instalado", + "installation.disabled": "El instalador del panel está deshabilitado en servidores públicos por defecto. Ejecute el instalador en una máquina local o habilítelo con la opción panel.install.", + "installation.issues.accounts": + "La carpeta /site/accounts no existe o no se puede escribir", + "installation.issues.content": + "La carpeta /content no existe o no se puede escribir", + "installation.issues.curl": "La extensión CURL es requerida", + "installation.issues.headline": "No se pudo instalar el panel", + "installation.issues.mbstring": + "La extension MB String es requerida", + "installation.issues.media": + "La carpeta /media no existe o no se puede escribir", + "installation.issues.php": "Asegúrate de usar PHP 7+", + "installation.issues.server": + "Kirby requiere Apache, Nginx o Caddy", + "installation.issues.sessions": "La carpeta /site/sessions no existe o no se puede escribir", + + "language": "Idioma", + "language.code": "Código", + "language.convert": "Hacer por defecto", + "language.convert.confirm": + "{name}
al idioma por defecto? Esto no se puede deshacer.

Si {name} tiene contenido sin traducir, ya no habrá un respaldo válido y algunas partes de su sitio podrían estar vacías.

", + "language.create": "Añadir un nuevo idioma", + "language.delete.confirm": + "¿De verdad quieres eliminar el idioma {name} incluyendo todas las traducciones? ¡Esto no se puede deshacer!", + "language.deleted": "El idioma ha sido eliminado", + "language.direction": "Leyendo dirección", + "language.direction.ltr": "De izquierda a derecha", + "language.direction.rtl": "De derecha a izquierda", + "language.locale": "PHP locale string", + "language.locale.warning": "You are using a custom locale set up. Please modify it in the language file in /site/languages", + "language.name": "Nombre", + "language.updated": "El idioma ha sido actualizado", + + "languages": "Idiomas", + "languages.default": "Idioma predeterminado", + "languages.empty": "Todavía no hay idiomas", + "languages.secondary": "Idiomas secundarios", + "languages.secondary.empty": "Todavía no hay idiomas secundarios", + + "license": "Licencia", + "license.buy": "Comprar una licencia", + "license.register": "Registro", + "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", + + "link": "Enlace", + "link.text": "Texto del enlace", + + "loading": "Cargando", + + "lock.unsaved": "Unsaved changes", + "lock.unsaved.empty": "There are no more unsaved changes", + "lock.isLocked": "Unsaved changes by {email}", + "lock.file.isLocked": "The file is currently being edited by {email} and cannot be changed.", + "lock.page.isLocked": "The page is currently being edited by {email} and cannot be changed.", + "lock.unlock": "Unlock", + "lock.isUnlocked": "Your unsaved changes have been overwritten by another user. You can download your changes to merge them manually.", + + "login": "Iniciar sesión", + "login.remember": "Mantener sesión iniciada", + + "logout": "Cerrar sesión", + + "menu": "Menu", + "meridiem": "AM/PM", + "mime": "Tipos de medios", + "minutes": "Minutos", + + "month": "Mes", + "months.april": "Abril", + "months.august": "Agosto", + "months.december": "Diciembre", + "months.february": "Febrero", + "months.january": "Enero", + "months.july": "Julio", + "months.june": "Junio", + "months.march": "Marzo", + "months.may": "Mayo", + "months.november": "Noviembre", + "months.october": "Octubre", + "months.september": "Septiembre", + + "more": "Más", + "name": "Nombre", + "next": "Siguiente", + "off": "off", + "on": "on", + "open": "Abrir", + "options": "Opciones", + + "orientation": "Orientación", + "orientation.landscape": "Paisaje", + "orientation.portrait": "Retrato", + "orientation.square": "Cuadrado", + + "page.changeSlug": "Cambiar URL", + "page.changeSlug.fromTitle": "Crear en base al título", + "page.changeStatus": "Cambiar estado", + "page.changeStatus.position": "Por favor seleccione una posición", + "page.changeStatus.select": "Seleccione un nuevo estado", + "page.changeTemplate": "Cambiar plantilla", + "page.delete.confirm": + "¿Realmente quieres eliminar {title}?", + "page.delete.confirm.subpages": + "Esta página tiene subpáginas.
Todas las subpáginas también serán eliminadas.", + "page.delete.confirm.title": "Introduzca el título de la página para confirmar", + "page.draft.create": "Crear borrador", + "page.duplicate.appendix": "Copiar", + "page.duplicate.files": "Copy files", + "page.duplicate.pages": "Copy pages", + "page.status": "Estado", + "page.status.draft": "Borrador", + "page.status.draft.description": + "La página está en modo borrador y solo está visible para los editores registrados", + "page.status.listed": "Publica", + "page.status.listed.description": "La página es pública para cualquiera", + "page.status.unlisted": "Sin publicar", + "page.status.unlisted.description": "La página solo es accesible vía URL", + + "pages": "Paginas", + "pages.empty": "Aún no hay páginas", + "pages.status.draft": "Borradores", + "pages.status.listed": "Publicadas", + "pages.status.unlisted": "Sin publicar", + + "pagination.page": "Página", + + "password": "Contraseña", + "pixel": "Pixel", + "prev": "Anterior", + "remove": "Eliminar", + "rename": "Renombrar", + "replace": "Remplazar", + "retry": "Inténtalo de nuevo", + "revert": "Revertir", + + "role": "Rol", + "role.admin.description": "The admin has all rights", + "role.admin.title": "Admin", + "role.all": "Todos", + "role.empty": "No hay usuarios con este rol", + "role.description.placeholder": "Sin descripción", + "role.nobody.description": "This is a fallback role without any permissions", + "role.nobody.title": "Nobody", + + "save": "Guardar", + "search": "Buscar", + + "section.required": "The section is required", + + "select": "Seleccionar", + "settings": "Ajustes", + "size": "Tamaño", + "slug": "Apéndice URL", + "sort": "Ordenar", + "title": "Titulo", + "template": "Plantilla", + "today": "Hoy", + + "toolbar.button.code": "Código", + "toolbar.button.bold": "Negritas", + "toolbar.button.email": "Correo electrónico", + "toolbar.button.headings": "Encabezados", + "toolbar.button.heading.1": "Encabezado 1", + "toolbar.button.heading.2": "Encabezado 2", + "toolbar.button.heading.3": "Encabezado 3", + "toolbar.button.italic": "Italica", + "toolbar.button.file": "Archivo", + "toolbar.button.file.select": "Seleccione un archivo", + "toolbar.button.file.upload": "Sube un archivo", + "toolbar.button.link": "Enlace", + "toolbar.button.ol": "Lista ordenada", + "toolbar.button.ul": "Lista de viñetas", + + "translation.author": "Turqueso", + "translation.direction": "ltr", + "translation.name": "Español", + "translation.locale": "es_ES", + + "upload": "Subir", + "upload.error.cantMove": "The uploaded file could not be moved", + "upload.error.cantWrite": "Failed to write file to disk", + "upload.error.default": "The file could not be uploaded", + "upload.error.extension": "File upload stopped by extension", + "upload.error.formSize": "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the form", + "upload.error.iniPostSize": "The uploaded file exceeds the post_max_size directive in php.ini", + "upload.error.iniSize": "The uploaded file exceeds the upload_max_filesize directive in php.ini", + "upload.error.noFile": "No file was uploaded", + "upload.error.noFiles": "No files were uploaded", + "upload.error.partial": "The uploaded file was only partially uploaded", + "upload.error.tmpDir": "Missing a temporary folder", + "upload.errors": "Error", + "upload.progress": "Cargando…", + + "url": "Url", + "url.placeholder": "https://ejemplo.com", + + "user": "Usuario", + "user.blueprint": + "Puede definir secciones adicionales y campos de formulario para este rol de usuario en /site/blueprints/users/{role}.yml", + "user.changeEmail": "Cambiar correo electrónico", + "user.changeLanguage": "Cambiar idioma", + "user.changeName": "Renombrar a este usuario", + "user.changePassword": "Cambia contraseña", + "user.changePassword.new": "Nueva contraseña", + "user.changePassword.new.confirm": "Confirmar nueva contraseña…", + "user.changeRole": "Cambiar rol", + "user.changeRole.select": "Seleccione un nuevo rol", + "user.create": "Añadir un nuevo usuario", + "user.delete": "Eliminar este usuario", + "user.delete.confirm": + "¿Realmente quieres eliminar
{email}?", + + "users": "Usuarios", + + "version": "Versión", + + "view.account": "Su cuenta", + "view.installation": "Instalación", + "view.settings": "Ajustes", + "view.site": "Sitio", + "view.users": "Usuarios", + + "welcome": "Bienvenido(a)", + "year": "Año" +} diff --git a/kirby/i18n/translations/fa.json b/kirby/i18n/translations/fa.json new file mode 100755 index 0000000..7e2dcda --- /dev/null +++ b/kirby/i18n/translations/fa.json @@ -0,0 +1,481 @@ +{ + "add": "\u0627\u0641\u0632\u0648\u062f\u0646", + "avatar": "\u062a\u0635\u0648\u06cc\u0631 \u067e\u0631\u0648\u0641\u0627\u06cc\u0644", + "back": "بازگشت", + "cancel": "\u0627\u0646\u0635\u0631\u0627\u0641", + "change": "\u0627\u0635\u0644\u0627\u062d", + "close": "\u0628\u0633\u062a\u0646", + "confirm": "تایید", + "copy": "کپی", + "create": "ایجاد", + + "date": "تاریخ", + "date.select": "یک تاریخ را انتخاب کنید", + + "day": "روز", + "days.fri": "\u062c\u0645\u0639\u0647", + "days.mon": "\u062f\u0648\u0634\u0646\u0628\u0647", + "days.sat": "\u0634\u0646\u0628\u0647", + "days.sun": "\u06cc\u06a9\u0634\u0646\u0628\u0647", + "days.thu": "\u067e\u0646\u062c\u0634\u0646\u0628\u0647", + "days.tue": "\u0633\u0647 \u0634\u0646\u0628\u0647", + "days.wed": "\u0686\u0647\u0627\u0631\u0634\u0646\u0628\u0647", + + "delete": "\u062d\u0630\u0641", + "dimensions": "ابعاد", + "disabled": "Disabled", + "discard": "\u0627\u0646\u0635\u0631\u0627\u0641", + "download": "Download", + "duplicate": "Duplicate", + "edit": "\u0648\u06cc\u0631\u0627\u06cc\u0634", + + "dialog.files.empty": "No files to select", + "dialog.pages.empty": "No pages to select", + "dialog.users.empty": "No users to select", + + "email": "\u067e\u0633\u062a \u0627\u0644\u06a9\u062a\u0631\u0648\u0646\u06cc\u06a9", + "email.placeholder": "mail@example.com", + + "error.access.login": "اطلاعات ورودی نامعتبر است", + "error.access.panel": "شما اجازه دسترسی به پانل را ندارید", + "error.access.view": "You are not allowed to access this part of the panel", + + "error.avatar.create.fail": "بارگزاری تصویر پروفایل موفق نبود", + "error.avatar.delete.fail": "\u062a\u0635\u0648\u06cc\u0631 \u067e\u0631\u0648\u0641\u0627\u06cc\u0644 \u0631\u0627 \u0646\u0645\u06cc\u062a\u0648\u0627\u0646 \u062d\u0630\u0641 \u06a9\u0631\u062f", + "error.avatar.dimensions.invalid": + "لطفا طول و عرض تصویر پروفایل را زیر 3000 پیکسل انتخاب کنید", + "error.avatar.mime.forbidden": + "تصویر پروفایل باید از نوع JPEG یا PNG باشد", + + "error.blueprint.notFound": "بلوپرینت با نام «{name}» قابل بارگذاری نیست", + + "error.email.preset.notFound": "قالب ایمیل «{name}» پیدا نشد", + + "error.field.converter.invalid": "مبدل «{converter}» نامعتبر است", + + "error.file.changeName.empty": "The name must not be empty", + "error.file.changeName.permission": + "شما اجازه تنغییر نام فایل «{filename}» را ندارید", + "error.file.duplicate": "فایلی هم نام با «{filename}» هم اکنون موجود است", + "error.file.extension.forbidden": + "پسوند فایل «{extension}» غیرمجاز است", + "error.file.extension.missing": + "\u0634\u0645\u0627 \u0646\u0645\u06cc\u200c\u062a\u0648\u0627\u0646\u06cc\u062f \u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u0628\u062f\u0648\u0646 \u067e\u0633\u0648\u0646\u062f \u0631\u0627 \u0622\u067e\u0644\u0648\u062f \u06a9\u0646\u06cc\u062f", + "error.file.maxheight": "The height of the image must not exceed {height} pixels", + "error.file.maxsize": "The file is too large", + "error.file.maxwidth": "The width of the image must not exceed {width} pixels", + "error.file.mime.differs": + "فایل آپلود شده باید از همان نوع باشد «{mime}»", + "error.file.mime.forbidden": "فرمت فایل «{mime}» غیرمجاز است", + "error.file.mime.invalid": "Invalid mime type: {mime}", + "error.file.mime.missing": + "فرمت فایل «{filename}» قابل شناسایی نیست", + "error.file.minheight": "The height of the image must be at least {height} pixels", + "error.file.minsize": "The file is too small", + "error.file.minwidth": "The width of the image must be at least {width} pixels", + "error.file.name.missing": "نام فایل اجباری است", + "error.file.notFound": "فایل «{filename}» پیدا نشد.", + "error.file.orientation": "The orientation of the image must be \"{orientation}\"", + "error.file.type.forbidden": "شما اجازه بارگذاری فایلهای «{type}» را ندارید", + "error.file.undefined": "\u0641\u0627\u06cc\u0644 \u0645\u0648\u0631\u062f \u0646\u0638\u0631 \u067e\u06cc\u062f\u0627 \u0646\u0634\u062f.", + + "error.form.incomplete": "لطفا کلیه خطاهای فرم را برطرف کنید", + "error.form.notSaved": "امکان دخیره فرم وجود ندارد", + + "error.language.code": "Please enter a valid code for the language", + "error.language.duplicate": "The language already exists", + "error.language.name": "Please enter a valid name for the language", + + "error.license.format": "Please enter a valid license key", + "error.license.email": "لطفا ایمیل صحیحی وارد کنید", + "error.license.verification": "The license could not be verified", + + "error.page.changeSlug.permission": + "شما امکان تغییر پسوند Url صفحه «{slug}» را ندارید", + "error.page.changeStatus.incomplete": + "صفحه حاوی خطا است و قابل انتشار نیست", + "error.page.changeStatus.permission": + "وضعیت صفحه جاری قابل تغییر نیست", + "error.page.changeStatus.toDraft.invalid": + "صفحه «{slug}» قابل تبدیل به پیش نویس نیست", + "error.page.changeTemplate.invalid": + "قالب صفحه «{slug}» قابل تغییر نیست", + "error.page.changeTemplate.permission": + "شما اجازه تغییر قالب «{slug}» را ندارید", + "error.page.changeTitle.empty": "عنوان اجباری است", + "error.page.changeTitle.permission": + "شما اجازه تغییر عنوان «{slug}» را ندارید", + "error.page.create.permission": "شما اجازه ایجاد «{slug}» را ندارید", + "error.page.delete": "حذف صفحه «{slug}» ممکن نیست", + "error.page.delete.confirm": "جهت ادامه عنوان صفحه را وارد کنید", + "error.page.delete.hasChildren": + "این صفحه جاوی زیرصفحه است و نمی تواند حذف شود", + "error.page.delete.permission": "شما اجازه حذف «{slug}» را ندارید", + "error.page.draft.duplicate": + "صفحه پیش‌نویسی با پسوند Url مشابه «{slug}» هم اکنون موجود است", + "error.page.duplicate": + "صفحه‌ای با آدرس Url مشابه «{slug}» هم اکنون موجود است", + "error.page.duplicate.permission": "You are not allowed to duplicate \"{slug}\"", + "error.page.notFound": "صفحه مورد نظر با آدرس «{slug}» پیدا نشد.", + "error.page.num.invalid": + "لطفا شماره ترتیب را بدرستی وارد نمایید. اعداد نباید منفی باشند.", + "error.page.slug.invalid": "لطفا یک پیشوند Url صحیح وارد کنید", + "error.page.sort.permission": "امکان مرتب‌سازی «{slug}» نیست", + "error.page.status.invalid": "لطفا وضعیت صحیحی برای صفحه انتخاب کنید", + "error.page.undefined": "صفحه مورد نظر پیدا نشد", + "error.page.update.permission": "شما اجازه بروزرسانی «{slug}» را ندارید", + + "error.section.files.max.plural": + "نباید بیش از {max} فایل به بخش «{section}» اضافه کنید", + "error.section.files.max.singular": + "نباید بیش از یک فایل به بخش «{section}» اضافه کنید", + "error.section.files.min.plural": + "The \"{section}\" section requires at least {min} files", + "error.section.files.min.singular": + "The \"{section}\" section requires at least one file", + + "error.section.pages.max.plural": + "نباید بیش از {max} صفحه به بخش «{section}» اضافه کنید", + "error.section.pages.max.singular": + "نباید بیش از یک صفحه به بخش «{section}» اضافه کنید", + "error.section.pages.min.plural": + "The \"{section}\" section requires at least {min} pages", + "error.section.pages.min.singular": + "The \"{section}\" section requires at least one page", + + "error.section.notLoaded": "بخش «{name}» قابل بارکذاری نیست", + "error.section.type.invalid": "نوع بخش «{type}» غیرمجاز است", + + "error.site.changeTitle.empty": "عنوان اجباری است", + "error.site.changeTitle.permission": + "شما اجازه تغییر عنوان سایت را ندارید", + "error.site.update.permission": "شما اجازه بروزرسانی سایت را ندارید", + + "error.template.default.notFound": "قالب پیش فرض موجود نیست", + + "error.user.changeEmail.permission": + "شما اجازه تغییر ایمیل کاربر «{name}» را ندارید", + "error.user.changeLanguage.permission": + "شما اجازه تغییر زبان برای کاربر «{name}» را ندارید", + "error.user.changeName.permission": + "شما اجازه تنغییر نام کاربر «{name}» را ندارید", + "error.user.changePassword.permission": + "شما اجازه تغییر رمز عبور کاربر «{name}» را ندارید", + "error.user.changeRole.lastAdmin": + "نقش آخرین مدیر سیستم قابل تغییر نیست", + "error.user.changeRole.permission": + "شما اجازه تغییر نقش کاربر «{name}» را ندارید", + "error.user.changeRole.toAdmin": + "You are not allowed to promote someone to the admin role", + "error.user.create.permission": "شما اجازه ایجاد این کاربر را ندارید", + "error.user.delete": "کاربر «{name}» نمی تواند حذف شود", + "error.user.delete.lastAdmin": "\u062d\u0630\u0641 \u0622\u062e\u0631\u06cc\u0646 \u0645\u062f\u06cc\u0631 \u0633\u06cc\u0633\u062a\u0645 \u0645\u0645\u06a9\u0646 \u0646\u06cc\u0633\u062a", + "error.user.delete.lastUser": "حذف آخرین کاربر ممکن نیست", + "error.user.delete.permission": + "شما اجازه حذف کاربر «{name}» را ندارید", + "error.user.duplicate": + "کاربری با ایمیل «{email}» هم اکنون موجود است", + "error.user.email.invalid": "لطفا یک ایمیل معتبر وارد کنید", + "error.user.language.invalid": "لطفا زبان معتبری انتخاب کنید", + "error.user.notFound": "کاربر «{name}» پیدا نشد", + "error.user.password.invalid": + "لطفا گذرواژه صحیحی با حداقل طول 8 حرف وارد کنید. ", + "error.user.password.notSame": "\u0644\u0637\u0641\u0627 \u062a\u06a9\u0631\u0627\u0631 \u06af\u0630\u0631\u0648\u0627\u0698\u0647 \u0631\u0627 \u0648\u0627\u0631\u062f \u0646\u0645\u0627\u06cc\u06cc\u062f", + "error.user.password.undefined": "کاربر فاقد گذرواژه است", + "error.user.role.invalid": "لطفا نقش صحیحی وارد نمایید", + "error.user.update.permission": + "شما اجازه بروزرسانی کاربر «{name}» را ندارید", + + "error.validation.accepted": "لطفا تایید کنید", + "error.validation.alpha": "لطفا تنها از بین حروف a-z انتخاب کنید", + "error.validation.alphanum": + "لطفا تنها از بین حروف a-z و اعداد 0-9 انتخاب کنید", + "error.validation.between": + "لطفا مقداری مابین «{min}» و «{max}» وارد کنید", + "error.validation.boolean": "لطفا تایید یا رد کنید", + "error.validation.contains": + "لطفا مقداری شامل «{needle}» وارد کنید", + "error.validation.date": "لطفا تاریخ معتبری وارد کنید", + "error.validation.date.after": "Please enter a date after {date}", + "error.validation.date.before": "Please enter a date before {date}", + "error.validation.date.between": "Please enter a date between {min} and {max}", + "error.validation.denied": "لطفا رد کنید", + "error.validation.different": "مقدار نباید مساوی «{other}» باشد", + "error.validation.email": "لطفا ایمیل صحیحی وارد کنید", + "error.validation.endswith": "مقدار باید با «{end}» ختم شود", + "error.validation.filename": "لطفا نام فایل صحیحی وارد کنید", + "error.validation.in": "لطفا یکی از مقادیر روبرو را وارد کنید: ({in})", + "error.validation.integer": "لطفا عدد صحیحی وارد کنید", + "error.validation.ip": "لطفا IP آدرس صحیحی وارد کنید", + "error.validation.less": "لطفا مقداری کمتر از {max} وارد کنید", + "error.validation.match": "مقدار وارد شده با الگوی مورد نظر همخوانی ندارد", + "error.validation.max": "لطفا مقداری کوچکتر یا مساوی {min} وارد کنید", + "error.validation.maxlength": + "لطفا عبارت کوتاه‌تری وارد کنید. (حداکثر {max} حرف)", + "error.validation.maxwords": "لطفا بیش از {max} کلمه وارد نکنید", + "error.validation.min": "لطفا مقداری بزرگتر یا مساوی با {min} وارد کنید", + "error.validation.minlength": + "لطفا عبارتی طولانی‌تری وارد کنید. (حداقل {min} حرف)", + "error.validation.minwords": "لطفا حداقل {min} کلمه وارد کنید", + "error.validation.more": "لطفا مقداری بیش از {min} وارد کنید", + "error.validation.notcontains": + "لطفا مقداری فاقد «{needle}» وارد کنید", + "error.validation.notin": + "لطفا از مقادیر روبرو استفاده نکنید: ({notin})", + "error.validation.option": "لطفا گزینه معتبری انتخاب کنید", + "error.validation.num": "لطفا عدد صحیحی وارد کنید", + "error.validation.required": "لطفا مقداری وارد کنید", + "error.validation.same": "لطفا مقدار «{other}» را وارد کنید", + "error.validation.size": "اندازه ورودی باید معادل «{size}» باشد", + "error.validation.startswith": "مقدار باید با «{start}» شروع شود", + "error.validation.time": "لطفا زمان معتبری وارد کنید", + "error.validation.url": "لطفا آدرس URL صحیح وارد کنید", + + "field.required": "The field is required", + "field.files.empty": "فایلی انتخاب نشده است", + "field.pages.empty": "صفحه‌ای انتخاب نشده است", + "field.structure.delete.confirm": "\u0645\u062f\u062e\u0644 \u062c\u0627\u0631\u06cc \u062d\u0630\u0641 \u0634\u0648\u062f\u061f", + "field.structure.empty": "\u0645\u0648\u0631\u062f\u06cc \u0648\u062c\u0648\u062f \u0646\u062f\u0627\u0631\u062f.", + "field.users.empty": "کاربری انتخاب نشده است", + + "file.delete.confirm": + "آیا واقعا می خواهید این فایل را حذف کنید؟
{filename}", + + "files": "فایل‌ها", + "files.empty": "فایلی موجود نیست", + + "hour": "ساعت", + "insert": "\u062f\u0631\u062c", + "install": "نصب", + + "installation": "نصب و راه اندازی", + "installation.completed": "پنل کاربری نصب شد", + "installation.disabled": "نصب کننده پانل کاربری بصورت پیش‌فرض در سرورهای عمومی غیرفعال است. لطفا نصب را روی یک ماشین محلی اجرا کنید یا آن را با استفاده از panel.install فعال کنید.", + "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.php": "لطفا از پی‌اچ‌پی 7 یا بالاتر استفاده کنید", + "installation.issues.server": + "کربی نیاز به Apache، Nginx یا Caddy دارد", + "installation.issues.sessions": "پوشه /site/sessions وجود ندارد یا قابل نوشتن نیست", + + "language": "\u0632\u0628\u0627\u0646", + "language.code": "کد", + "language.convert": "پیش‌فرض شود", + "language.convert.confirm": + "

آیا واقعا میخواهید {name} را به زبان پیشفرض تبدیل کنید؟ این عمل برگشت ناپذیر است.

اگر {name} دارای محتوای غیر ترجمه شده باشد، جایگزین معتبر دیگری نخواهد بود و ممکن است بخش‌هایی از سایت شما خالی باشد.

", + "language.create": "افزودن زبان جدید", + "language.delete.confirm": + "آیا واقعا میخواهید زبان {name} را به همراه تمام ترجمه‌ها حذف کنید؟ این عمل قابل بازگشت نخواهد بود!", + "language.deleted": "زبان مورد نظر حذف شد", + "language.direction": "rtl", + "language.direction.ltr": "چپ به راست", + "language.direction.rtl": "راست به چپ", + "language.locale": "PHP locale string", + "language.locale.warning": "You are using a custom locale set up. Please modify it in the language file in /site/languages", + "language.name": "پارسی", + "language.updated": "زبان به روز شد", + + "languages": "زبان‌ها", + "languages.default": "زبان پیش‌فرض", + "languages.empty": "هنوز هیچ زبانی موجود نیست", + "languages.secondary": "زبان‌های ثانویه", + "languages.secondary.empty": "هنوز هیچ زبان ثانویه‌ای موجود نیست", + + "license": "\u0645\u062c\u0648\u0632", + "license.buy": "خرید مجوز", + "license.register": "ثبت", + "license.register.help": + "پس از خرید از طریق ایمیل، کد مجوز خود را دریافت کردید. لطفا برای ثبت‌نام آن را کپی و اینجا پیست کنید.", + "license.register.label": "لطفا کد مجوز خود را وارد کنید", + "license.register.success": "با تشکر از شما برای حمایت از کربی", + "license.unregistered": "این یک نسخه آزمایشی ثبت نشده از کربی است", + + "link": "\u067e\u06cc\u0648\u0646\u062f", + "link.text": "\u0645\u062a\u0646 \u067e\u06cc\u0648\u0646\u062f", + + "loading": "بارگزاری", + + "lock.unsaved": "Unsaved changes", + "lock.unsaved.empty": "There are no more unsaved changes", + "lock.isLocked": "Unsaved changes by {email}", + "lock.file.isLocked": "The file is currently being edited by {email} and cannot be changed.", + "lock.page.isLocked": "The page is currently being edited by {email} and cannot be changed.", + "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.remember": "مرا به خاطر بسپار", + + "logout": "خروج", + + "menu": "منو", + "meridiem": "ق.ظ/ب.ظ", + "mime": "نوع رسانه", + "minutes": "دقیقه", + + "month": "ماه", + "months.april": "\u0622\u0648\u0631\u06cc\u0644", + "months.august": "\u0627\u0648\u062a", + "months.december": "\u062f\u0633\u0627\u0645\u0628\u0631", + "months.february": "\u0641\u0648\u0631\u06cc\u0647", + "months.january": "\u0698\u0627\u0646\u0648\u06cc\u0647", + "months.july": "\u0698\u0648\u0626\u06cc\u0647", + "months.june": "\u0698\u0648\u0626\u0646", + "months.march": "\u0645\u0627\u0631\u0633", + "months.may": "\u0645\u06cc", + "months.november": "\u0646\u0648\u0627\u0645\u0628\u0631", + "months.october": "\u0627\u06a9\u062a\u0628\u0631", + "months.september": "\u0633\u067e\u062a\u0627\u0645\u0628\u0631", + + "more": "بیشتر", + "name": "نام", + "next": "بعدی", + "off": "off", + "on": "on", + "open": "بازکردن", + "options": "گزینه‌ها", + + "orientation": "جهت", + "orientation.landscape": "افقی", + "orientation.portrait": "عمودی", + "orientation.square": "مربع", + + "page.changeSlug": "تغییر Url صفحه", + "page.changeSlug.fromTitle": "\u0627\u06cc\u062c\u0627\u062f \u0627\u0632 \u0631\u0648\u06cc \u0639\u0646\u0648\u0627\u0646", + "page.changeStatus": "تغییر وضعیت", + "page.changeStatus.position": "لطفا یک موقعیت را انتخاب کنید", + "page.changeStatus.select": "یک وضعیت جدید را انتخاب کنید", + "page.changeTemplate": "تغییر قالب", + "page.delete.confirm": + "صفحه {title} حذف شود؟", + "page.delete.confirm.subpages": + "این صفحه دارای زیرصفحه است.
تمام زیر صفحات نیز حذف خواهد شد.", + "page.delete.confirm.title": "جهت ادامه عنوان صفحه را وارد کنید", + "page.draft.create": "ایجاد پیش‌نویس", + "page.duplicate.appendix": "کپی", + "page.duplicate.files": "Copy files", + "page.duplicate.pages": "Copy pages", + "page.status": "وضعیت", + "page.status.draft": "پیش‌نویس", + "page.status.draft.description": + "این صفحه در حالت پیش‌نویس است و تنها برای ویرایشگران وارد شده قابل مشاهده است", + "page.status.listed": "عمومی", + "page.status.listed.description": "این صفحه برای عموم قابل مشاهده است", + "page.status.unlisted": "فهرست نشده", + "page.status.unlisted.description": "این صفحه فقط از طریق URL قابل دسترسی است", + + "pages": "صفحات", + "pages.empty": "هنوز هیچ صفحه‌ای موجود نیست", + "pages.status.draft": "پیش‌نویس‌ها", + "pages.status.listed": "منتشر شده", + "pages.status.unlisted": "فهرست نشده", + + "pagination.page": "صفحه", + + "password": "\u06af\u0630\u0631\u0648\u0627\u0698\u0647", + "pixel": "پیکسل", + "prev": "قبلی", + "remove": "حذف", + "rename": "تغییر نام", + "replace": "\u062c\u0627\u06cc\u06af\u0632\u06cc\u0646\u06cc", + "retry": "\u062a\u0644\u0627\u0634 \u0645\u062c\u062f\u062f", + "revert": "بازگرداندن تغییرات", + + "role": "\u0646\u0642\u0634", + "role.admin.description": "The admin has all rights", + "role.admin.title": "Admin", + "role.all": "همه", + "role.empty": "هیچ کاربری با این نقش وجود ندارد", + "role.description.placeholder": "فاقد شرح", + "role.nobody.description": "This is a fallback role without any permissions", + "role.nobody.title": "Nobody", + + "save": "\u0630\u062e\u06cc\u0631\u0647", + "search": "جستجو", + + "section.required": "The section is required", + + "select": "انتخاب", + "settings": "تنظیمات", + "size": "اندازه", + "slug": "پسوند Url", + "sort": "ترتیب", + "title": "عنوان", + "template": "\u0642\u0627\u0644\u0628 \u0635\u0641\u062d\u0647", + "today": "امروز", + + "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", + "toolbar.button.headings": "عنوان‌ها", + "toolbar.button.heading.1": "عنوان 1", + "toolbar.button.heading.2": "عنوان 2", + "toolbar.button.heading.3": "عنوان 3", + "toolbar.button.italic": "\u0645\u062a\u0646 \u0627\u0631\u06cc\u0628", + "toolbar.button.file": "فایل", + "toolbar.button.file.select": "Select a file", + "toolbar.button.file.upload": "Upload a file", + "toolbar.button.link": "\u067e\u06cc\u0648\u0646\u062f", + "toolbar.button.ol": "لیست مرتب", + "toolbar.button.ul": "لیست معمولی", + + "translation.author": "تیم کربی", + "translation.direction": "rtl", + "translation.name": "انگلیسی", + "translation.locale": "fa_IR", + + "upload": "بارگذاری", + "upload.error.cantMove": "The uploaded file could not be moved", + "upload.error.cantWrite": "Failed to write file to disk", + "upload.error.default": "The file could not be uploaded", + "upload.error.extension": "File upload stopped by extension", + "upload.error.formSize": "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the form", + "upload.error.iniPostSize": "The uploaded file exceeds the post_max_size directive in php.ini", + "upload.error.iniSize": "The uploaded file exceeds the upload_max_filesize directive in php.ini", + "upload.error.noFile": "No file was uploaded", + "upload.error.noFiles": "No files were uploaded", + "upload.error.partial": "The uploaded file was only partially uploaded", + "upload.error.tmpDir": "Missing a temporary folder", + "upload.errors": "خطا", + "upload.progress": "در حال بارگذاری...", + + "url": "Url", + "url.placeholder": "https://example.com", + + "user": "کاربر", + "user.blueprint": + "شما می توانید قسمت‌های اضافی و فیلدهای فرم را برای این نقش کاربر در /site/blueprints/users/{role}.yml تعریف کنید", + "user.changeEmail": "تغییر ایمیل", + "user.changeLanguage": "تغییر زبان", + "user.changeName": "تغییر نام این کاربر", + "user.changePassword": "تغییر گذرواژه", + "user.changePassword.new": "گذرواژه جدید", + "user.changePassword.new.confirm": "تایید گذرواژه جدید...", + "user.changeRole": "تغییر نقش", + "user.changeRole.select": "یک نقش جدید را انتخاب کنید", + "user.create": "افزودن کاربر جدید", + "user.delete": "حذف کاربر جاری", + "user.delete.confirm": + "آیا واقعا میخواهید {email} را حذف کنید؟", + + "users": "کاربران", + + "version": "\u0646\u0633\u062e\u0647 \u0646\u0631\u0645 \u0627\u0641\u0632\u0627\u0631", + + "view.account": "حساب کاربری شما", + "view.installation": "\u0646\u0635\u0628 \u0648 \u0631\u0627\u0647 \u0627\u0646\u062f\u0627\u0632\u06cc", + "view.settings": "تنظیمات", + "view.site": "سایت", + "view.users": "\u06a9\u0627\u0631\u0628\u0631\u0627\u0646", + + "welcome": "خوش آمدید", + "year": "سال" +} diff --git a/kirby/i18n/translations/fi.json b/kirby/i18n/translations/fi.json new file mode 100755 index 0000000..a9794ac --- /dev/null +++ b/kirby/i18n/translations/fi.json @@ -0,0 +1,481 @@ +{ + "add": "Lis\u00e4\u00e4", + "avatar": "Profiilikuva", + "back": "Takaisin", + "cancel": "Peruuta", + "change": "Muuta", + "close": "Sulje", + "confirm": "Ok", + "copy": "Kopioi", + "create": "Luo", + + "date": "Päivämäärä", + "date.select": "Valitse päivämäärä", + + "day": "Päivä", + "days.fri": "Pe", + "days.mon": "Ma", + "days.sat": "La", + "days.sun": "Su", + "days.thu": "To", + "days.tue": "Ti", + "days.wed": "Ke", + + "delete": "Poista", + "dimensions": "Mitat", + "disabled": "Disabled", + "discard": "Hylkää", + "download": "Lataa", + "duplicate": "Kahdenna", + "edit": "Muokkaa", + + "dialog.files.empty": "Ei valittavissa olevia tiedostoja", + "dialog.pages.empty": "Ei valittavissa olevia sivuja", + "dialog.users.empty": "Ei valittavissa olevia käyttäjiä", + + "email": "S\u00e4hk\u00f6posti", + "email.placeholder": "nimi@osoite.fi", + + "error.access.login": "Kirjautumistiedot eivät kelpaa", + "error.access.panel": "Sinulla ei ole oikeutta käyttää paneelia", + "error.access.view": "Sinulla ei ole oikeutta käyttää tätä osaa paneelista", + + "error.avatar.create.fail": "Profiilikuvaa ei voitu lähettää", + "error.avatar.delete.fail": "Profiilikuvaa ei voitu poistaa", + "error.avatar.dimensions.invalid": + "Profiilikuvan leveys ja korkeus voivat olla enintään 3000 pikseliä", + "error.avatar.mime.forbidden": + "Profiilikuvan täytyy olla joko JPEG- tai PNG-formaatissa", + + "error.blueprint.notFound": "Kaavaa \"{name}\" ei voitu ladata", + + "error.email.preset.notFound": "Nimellä \"{name}\" ja kyseisellä verkkotunnuksella ei löydy sähköpostiosoitetta", + + "error.field.converter.invalid": "Muunnin \"{converter}\" ei kelpaa", + + "error.file.changeName.empty": "Nimi ei voi olla tyhjä", + "error.file.changeName.permission": + "Sinulla ei ole oikeutta muuttaa tiedoston \"{filename}\" nimeä", + "error.file.duplicate": "Tiedosto nimellä \"{filename}\" on jo olemassa", + "error.file.extension.forbidden": + "Tiedostopääte \"{extension}\" ei ole sallittu", + "error.file.extension.missing": + "Tiedoston \"{filename}\" tiedostopääte puuttuu", + "error.file.maxheight": "The height of the image must not exceed {height} pixels", + "error.file.maxsize": "The file is too large", + "error.file.maxwidth": "The width of the image must not exceed {width} pixels", + "error.file.mime.differs": + "Lähetetyllä tiedostolla täytyy olla sama mime-tyyppi \"{mime}\"", + "error.file.mime.forbidden": "Median tyyppi \"{mime}\" ei ole sallittu", + "error.file.mime.invalid": "Invalid mime type: {mime}", + "error.file.mime.missing": + "Tiedoston \"{filename}\" mediatyyppiä ei voida tunnistaa", + "error.file.minheight": "The height of the image must be at least {height} pixels", + "error.file.minsize": "The file is too small", + "error.file.minwidth": "The width of the image must be at least {width} pixels", + "error.file.name.missing": "Tiedostonimi ei voi olla tyhjä", + "error.file.notFound": "Tiedostoa \"{filename}\" ei löytynyt", + "error.file.orientation": "The orientation of the image must be \"{orientation}\"", + "error.file.type.forbidden": "Sinulla ei ole oikeutta lähettää tiedostoja joiden tyyppi on {type}", + "error.file.undefined": "Tiedostoa ei l\u00f6ytynyt", + + "error.form.incomplete": "Korjaa kaikki lomakkeen virheet...", + "error.form.notSaved": "Lomaketta ei voitu tallentaa", + + "error.language.code": "Anna kielen lyhenne", + "error.language.duplicate": "Kieli on jo olemassa", + "error.language.name": "Anna kielen nimi", + + "error.license.format": "Anna lisenssiavain", + "error.license.email": "Anna kelpaava sähköpostiosoite", + "error.license.verification": "Lisenssiä ei voitu vahvistaa", + + "error.page.changeSlug.permission": + "Sinulla ei ole oikeutta muuttaa URL-liitettä sivulle \"{slug}\"", + "error.page.changeStatus.incomplete": + "Sivulla on virheitä eikä sitä voitu julkaista", + "error.page.changeStatus.permission": + "Tämän sivun tilaa ei voi muuttaa", + "error.page.changeStatus.toDraft.invalid": + "Sivua \"{slug}\" ei voi muuttaa luonnokseksi", + "error.page.changeTemplate.invalid": + "Sivun \"{slug}\" pohjaa ei voi muuttaa", + "error.page.changeTemplate.permission": + "Sinulla ei ole oikeutta muuttaa sivun \"{slug}\" sivupohjaa", + "error.page.changeTitle.empty": "Nimi ei voi olla tyhjä", + "error.page.changeTitle.permission": + "Sinulla ei ole oikeutta muuttaa sivun \"{slug}\" nimeä", + "error.page.create.permission": "Sinulla ei ole oikeutta luoda sivua \"{slug}\"", + "error.page.delete": "Sivua \"{slug}\" ei voi poistaa", + "error.page.delete.confirm": "Anna vahvistuksena sivun nimi", + "error.page.delete.hasChildren": + "Sivu sisältää alasivuja eikä sitä voida poistaa", + "error.page.delete.permission": "Sinulla ei ole oikeutta poistaa sivua \"{slug}\"", + "error.page.draft.duplicate": + "Sivuluonnos URL-liitteellä \"{slug}\" on jo olemassa", + "error.page.duplicate": + "Sivu URL-liitteellä \"{slug}\" on jo olemassa", + "error.page.duplicate.permission": "Sinulla ei ole oikeutta kahdentaa sivua \"{slug}\"", + "error.page.notFound": "Sivua \"{slug}\" ei löytynyt", + "error.page.num.invalid": + "Anna kelpaava järjestysnumero. Numero ei voi olla negatiivinen.", + "error.page.slug.invalid": "Anna kelpaava URL-etuliite", + "error.page.sort.permission": "Sivua \"{slug}\" ei voi järjestellä", + "error.page.status.invalid": "Aseta kelvollinen sivun tila", + "error.page.undefined": "Sivua ei l\u00f6ytynyt", + "error.page.update.permission": "Sinulla ei ole oikeutta päivittää sivua \"{slug}\"", + + "error.section.files.max.plural": + "Et voi lisätä enemmän kuin {max} tiedostoa osioon \"{section}\"", + "error.section.files.max.singular": + "Et voi lisätä enempää kuin yhden tiedoston osioon \"{section}\"", + "error.section.files.min.plural": + "Osio \"{section}\" vaatii ainakin {min} tiedostoa", + "error.section.files.min.singular": + "Osio \"{section}\" vaatii ainakin yhden sivun", + + "error.section.pages.max.plural": + "Et voi lisätä enemmän kuin {max} sivua osioon \"{section}\"", + "error.section.pages.max.singular": + "Et voi lisätä enempää kuin yhden sivun osioon \"{section}\"", + "error.section.pages.min.plural": + "Osio \"{section}\" vaatii ainakin {min} sivua", + "error.section.pages.min.singular": + "Osio \"{section}\" vaatii ainakin yhden sivun", + + "error.section.notLoaded": "Osiota \"{name}\" ei voitu ladata", + "error.section.type.invalid": "Osion tyyppi \"{type}\" ei ole kelvollinen", + + "error.site.changeTitle.empty": "Nimi ei voi olla tyhjä", + "error.site.changeTitle.permission": + "Sinulla ei ole oikeutta päivittää sivuston nimeä", + "error.site.update.permission": "Sinulla ei ole oikeutta päivittää sivuston tietoja", + + "error.template.default.notFound": "Oletussivupohjaa ei ole määritetty", + + "error.user.changeEmail.permission": + "Sinulla ei ole oikeutta vaihtaa käyttäjän \"{name}\" sähköpostiosoitetta", + "error.user.changeLanguage.permission": + "Sinulla ei ole oikeutta vaihtaa käyttäjän \"{name}\" kieltä", + "error.user.changeName.permission": + "Sinulla ei ole oikeutta vaihtaa käyttäjän \"{name}\" nimeä", + "error.user.changePassword.permission": + "Sinulla ei ole oikeutta vaihtaa käyttäjän \"{name}\" salasanaa", + "error.user.changeRole.lastAdmin": + "Ainoan pääkäyttäjän roolia ei voi muuttaa", + "error.user.changeRole.permission": + "Sinulla ei ole oikeutta vaihtaa käyttäjän \"{name}\" käyttäjätasoa", + "error.user.changeRole.toAdmin": + "Sinulla ei ole oikeutta vaihtaa käyttäjätasoa pääkäyttäjäksi", + "error.user.create.permission": "Sinulla ei ole oikeutta luoda tätä käyttäjää", + "error.user.delete": "Käyttäjää \"{name}\" ei voi poistaa", + "error.user.delete.lastAdmin": "Ainoaa pääkäyttäjää ei voi poistaa", + "error.user.delete.lastUser": "Ainoaa käyttäjää ei voi poistaa", + "error.user.delete.permission": + "Sinulla ei ole oikeutta poistaa käyttäjää \"{name}\"", + "error.user.duplicate": + "Käyttäjä, jonka sähköpostiosoite on \"{name}\", on jo olemassa", + "error.user.email.invalid": "Anna kelpaava sähköpostiosoite", + "error.user.language.invalid": "Anna kelpaava kieli", + "error.user.notFound": "K\u00e4ytt\u00e4j\u00e4\u00e4 ei l\u00f6ytynyt", + "error.user.password.invalid": + "Anna kelpaava salasana. Salasanan täytyy olla ainakin 8 merkkiä pitkä.", + "error.user.password.notSame": "Salasanat eivät täsmää", + "error.user.password.undefined": "Käyttäjällä ei ole salasanaa", + "error.user.role.invalid": "Anna kelpaava käyttäjätaso", + "error.user.update.permission": + "Sinulla ei ole oikeutta päivittää käyttäjää \"{name}\"", + + "error.validation.accepted": "Ole hyvä ja vahvista", + "error.validation.alpha": "Anna vain merkkejä väliltä a-z", + "error.validation.alphanum": + "Anna vain merkkejä väliltä a-z tai/ja numeroita väliltä 0-9", + "error.validation.between": + "Anna arvo väliltä \"{min}\" ja \"{max}\"", + "error.validation.boolean": "Vahvista tai peruuta", + "error.validation.contains": + "Anna arvo joka sisältää \"{needle}\"", + "error.validation.date": "Anna kelpaava päivämäärä", + "error.validation.date.after": "Anna päivämäärä {date} jälkeen", + "error.validation.date.before": "Anna päivämäärä ennen {date}", + "error.validation.date.between": "Anna päivämäärä väliltä {min} ja {max}", + "error.validation.denied": "Ole hyvä ja peruuta", + "error.validation.different": "Arvo ei voi olla \"{other}\"", + "error.validation.email": "Anna kelpaava sähköpostiosoite", + "error.validation.endswith": "Arvon loppuosa täytyy olla \"{end}\"", + "error.validation.filename": "Anna kelpaava tiedostonimi", + "error.validation.in": "Anna joku seuraavista: ({in})", + "error.validation.integer": "Anna kelpaava kokonaisluku", + "error.validation.ip": "Anna kelpaava IP-osoite", + "error.validation.less": "Anna arvo joka on pienempi kuin {max}", + "error.validation.match": "Arvo ei vastaa vaadittua kaavaa", + "error.validation.max": "Anna arvo joka on enintään {max}", + "error.validation.maxlength": + "Anna lyhyempi arvo. (enintään {max} merkkiä)", + "error.validation.maxwords": "Anna korkeintaan {max} sana(a)", + "error.validation.min": "Anna arvo joka on vähintään {min}", + "error.validation.minlength": + "Anna pidempi arvo. (vähintään {min} merkkiä)", + "error.validation.minwords": "Anna vähintään {min} sana(a)", + "error.validation.more": "Anna suurempi arvo kuin {min}", + "error.validation.notcontains": + "Anna arvo joka ei sisällä \"{needle}\"", + "error.validation.notin": + "Arvo ei voi sisältää mitään seuraavista: ({notIn})", + "error.validation.option": "Valitse kelpaava vaihtoehto", + "error.validation.num": "Anna kelpaava numero", + "error.validation.required": "Arvo ei voi olla tyhjä", + "error.validation.same": "Anna \"{other}\"", + "error.validation.size": "Arvon koko täytyy olla \"{size}\"", + "error.validation.startswith": "Arvon alkuosa täytyy olla \"{start}\"", + "error.validation.time": "Anna kelpaava aika", + "error.validation.url": "Anna kelpaava URL", + + "field.required": "Kenttä on pakollinen", + "field.files.empty": "Tiedostoja ei ole vielä valittu", + "field.pages.empty": " Sivuja ei ole vielä valittu", + "field.structure.delete.confirm": "Haluatko varmasti poistaa tämän rivin?", + "field.structure.empty": "Rivejä ei ole vielä lisätty", + "field.users.empty": "Käyttäjiä ei ole vielä valittu", + + "file.delete.confirm": + "Haluatko varmasti poistaa tiedoston
{filename}?", + + "files": "Tiedostot", + "files.empty": "Tiedostoja ei ole vielä lisätty", + + "hour": "Tunti", + "insert": "Lis\u00e4\u00e4", + "install": "Asenna", + + "installation": "Asennus", + "installation.completed": "Paneeli on asennettu", + "installation.disabled": "Paneelin asennus on oletuksena poissa käytöstä julkisilla palvelimilla. Aja asennus paikallisella koneella, tai ota paneeli käyttöön panel.install-optiolla.", + "installation.issues.accounts": + "/site/accounts -kansio ei ole olemassa tai siihen ei voi kirjoittaa", + "installation.issues.content": + "/content -kansio ei ole olemassa tai siihen ei voi kirjoittaa", + "installation.issues.curl": "CURL-laajennos on pakollinen", + "installation.issues.headline": "Paneelia ei voida asentaa", + "installation.issues.mbstring": + "MB String-laajennos on pakollinen", + "installation.issues.media": + "/media -kansio ei ole olemassa tai siihen ei voi kirjoittaa", + "installation.issues.php": "Varmista että PHP 7+ on käytössä", + "installation.issues.server": + "Kirby tarvitsee jonkun seuraavista: Apache, Nginx tai Caddy", + "installation.issues.sessions": "/site/sessions -kansio ei ole olemassa tai siihen ei voi kirjoittaa", + + "language": "Kieli", + "language.code": "Tunniste", + "language.convert": "Muuta oletukseksi", + "language.convert.confirm": + "

Haluatko varmasti muuttaa kielen {name} oletuskieleksi? Tätä muutosta ei voi peruuttaa.

Jos{name} sisältää kääntämättömiä kohtia, varakäännöstä ei enää ole näille kohdille ja sivustosi saattaa olla osittain tyhjä.

", + "language.create": "Lisää uusi kieli", + "language.delete.confirm": + "Haluatko varmasti poistaa kielen {name}, mukaanlukien kaikki käännökset? Tätä toimintoa ei voi peruuttaa!", + "language.deleted": "Kieli on poistettu", + "language.direction": "Lukusuunta", + "language.direction.ltr": "Vasemmalta oikealle", + "language.direction.rtl": "Oikealta vasemmalle", + "language.locale": "PHP-lokaalin tunniste", + "language.locale.warning": "You are using a custom locale set up. Please modify it in the language file in /site/languages", + "language.name": "Nimi", + "language.updated": "Kieli on päivitetty", + + "languages": "Kielet", + "languages.default": "Oletuskieli", + "languages.empty": "Kieliä ei ole vielä määritetty", + "languages.secondary": "Toissijaiset kielet", + "languages.secondary.empty": "Toissijaisia kieliä ei ole vielä määritetty", + + "license": "Lisenssi", + "license.buy": "Osta lisenssi", + "license.register": "Rekisteröi", + "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ä", + + "link": "Linkki", + "link.text": "Linkin teksti", + + "loading": "Ladataan", + + "lock.unsaved": "Tallentamattomia muutoksia", + "lock.unsaved.empty": "There are no more unsaved changes", + "lock.isLocked": "Käyttäjällä {email} on tallentamattomia muutoksia", + "lock.file.isLocked": "The file is currently being edited by {email} and cannot be changed.", + "lock.page.isLocked": "The page is currently being edited by {email} and cannot be changed.", + "lock.unlock": "Vapauta", + "lock.isUnlocked": "Your unsaved changes have been overwritten by another user. You can download your changes to merge them manually.", + + "login": "Kirjaudu", + "login.remember": "Pidä minut kirjautuneena", + + "logout": "Kirjaudu ulos", + + "menu": "Valikko", + "meridiem": "am/pm", + "mime": "Median tyyppi", + "minutes": "Minuutit", + + "month": "Kuukausi", + "months.april": "Huhtikuu", + "months.august": "Elokuu", + "months.december": "Joulukuu", + "months.february": "Helmikuu", + "months.january": "Tammikuu", + "months.july": "Hein\u00e4kuu", + "months.june": "Kes\u00e4kuu", + "months.march": "Maaliskuu", + "months.may": "Toukokuu", + "months.november": "Marraskuu", + "months.october": "Lokakuu", + "months.september": "Syyskuu", + + "more": "Lisää", + "name": "Nimi", + "next": "Seuraava", + "off": "off", + "on": "on", + "open": "Avaa", + "options": "Asetukset", + + "orientation": "Suunta", + "orientation.landscape": "Vaakasuuntainen", + "orientation.portrait": "Pystysuuntainen", + "orientation.square": "Neliskulmainen", + + "page.changeSlug": "Vaihda URL-osoite", + "page.changeSlug.fromTitle": "Luo nimen perusteella", + "page.changeStatus": "Muuta tilaa", + "page.changeStatus.position": "Valitse järjestyspaikka", + "page.changeStatus.select": "Valitse uusi tila", + "page.changeTemplate": "Vaihda sivupohja", + "page.delete.confirm": + "Haluatko varmasti poistaa sivun {title}?", + "page.delete.confirm.subpages": + "Tällä sivulla on alasivuja.
Myös kaikki alasivut poistetaan.", + "page.delete.confirm.title": "Anna vahvistuksena sivun nimi", + "page.draft.create": "Uusi luonnos", + "page.duplicate.appendix": "Kopioi", + "page.duplicate.files": "Copy files", + "page.duplicate.pages": "Copy pages", + "page.status": "Tila", + "page.status.draft": "Luonnos", + "page.status.draft.description": + "Sivu on luonnostilassa ja näkyy vain kirjautuneille muokkaajille", + "page.status.listed": "Julkinen", + "page.status.listed.description": "Sivu on julkinen kaikille", + "page.status.unlisted": "Listaamaton", + "page.status.unlisted.description": "Sivulle pääsee vain URL:n kautta", + + "pages": "Sivut", + "pages.empty": "Sivuja ei ole vielä lisätty", + "pages.status.draft": "Luonnokset", + "pages.status.listed": "Julkaistut", + "pages.status.unlisted": "Listaamaton", + + "pagination.page": "Sivu", + + "password": "Salasana", + "pixel": "Pikseli", + "prev": "Edellinen", + "remove": "Poista", + "rename": "Nimeä uudelleen", + "replace": "Korvaa", + "retry": "Yrit\u00e4 uudelleen", + "revert": "Palauta", + + "role": "K\u00e4ytt\u00e4j\u00e4taso", + "role.admin.description": "Pääkäyttäjällä on kaikki oikeudet", + "role.admin.title": "Pääkäyttäjä", + "role.all": "Kaikki", + "role.empty": "Tällä käyttäjätasolla ei ole yhtään käyttäjää", + "role.description.placeholder": "Ei kuvausta", + "role.nobody.description": "This is a fallback role without any permissions", + "role.nobody.title": "Nobody", + + "save": "Tallenna", + "search": "Haku", + + "section.required": "Osio on pakollinen", + + "select": "Valitse", + "settings": "Asetukset", + "size": "Koko", + "slug": "URL-tunniste", + "sort": "Järjestele", + "title": "Nimi", + "template": "Sivupohja", + "today": "Tänään", + + "toolbar.button.code": "Koodi", + "toolbar.button.bold": "Lihavointi", + "toolbar.button.email": "S\u00e4hk\u00f6posti", + "toolbar.button.headings": "Otsikot", + "toolbar.button.heading.1": "Otsikko 1", + "toolbar.button.heading.2": "Otsikko 2", + "toolbar.button.heading.3": "Otsikko 3", + "toolbar.button.italic": "Kursivointi", + "toolbar.button.file": "Tiedosto", + "toolbar.button.file.select": "Valitse tiedosto", + "toolbar.button.file.upload": "Lähetä tiedosto", + "toolbar.button.link": "Linkki", + "toolbar.button.ol": "Järjestetty lista", + "toolbar.button.ul": "Järjestämätön lista", + + "translation.author": "Kirby-tiimi", + "translation.direction": "ltr", + "translation.name": "Suomi", + "translation.locale": "fi_FI", + + "upload": "Lähetä", + "upload.error.cantMove": "Lähetettyä tiedostoa ei voitu siirtää", + "upload.error.cantWrite": "Tiedoston kirjoitus levylle epäonnistui", + "upload.error.default": "Tiedostoa ei voitu lähettää", + "upload.error.extension": "File upload stopped by extension", + "upload.error.formSize": "Lähetetyn tiedoston koko ylittää lomakkeen sallitun ylärajan MAX_FILE_SIZE", + "upload.error.iniPostSize": "Lähetetyn tiedoston koko ylittää sallitun ylärajan post_max_size asetustiedostossa php.ini", + "upload.error.iniSize": "Lähetetyn tiedoston koko ylittää sallitun ylärajan upload_max_filesize asetustiedostossa php.ini", + "upload.error.noFile": "Tiedostoa ei lähetetty", + "upload.error.noFiles": "Tiedostoja ei lähetetty", + "upload.error.partial": "The uploaded file was only partially uploaded", + "upload.error.tmpDir": "Missing a temporary folder", + "upload.errors": "Virhe", + "upload.progress": "Lähetetään...", + + "url": "Url", + "url.placeholder": "https://esimerkki.fi", + + "user": "Käyttäjä", + "user.blueprint": + "Voit määrittää lisää osioita ja lomakekenttiä tälle käyttäjälle kaavassa /site/blueprints/users/{role}.yml", + "user.changeEmail": "Muuta sähköpostiosoite", + "user.changeLanguage": "Vaihda kieli", + "user.changeName": "Nimeä uudelleen", + "user.changePassword": "Vaihda salasana", + "user.changePassword.new": "Uusi salasana", + "user.changePassword.new.confirm": "Vahvista uusi salasana...", + "user.changeRole": "Muuta käyttäjätasoa", + "user.changeRole.select": "Valitse uusi käyttäjätaso", + "user.create": "Lisää uusi käyttäjä", + "user.delete": "Poista tämä käyttäjä", + "user.delete.confirm": + "Haluatko varmsti poistaa käyttäjän
{email}?", + + "users": "Käyttäjät", + + "version": "Versio", + + "view.account": "Oma käyttäjätili", + "view.installation": "Asennus", + "view.settings": "Asetukset", + "view.site": "Sivusto", + "view.users": "K\u00e4ytt\u00e4j\u00e4t", + + "welcome": "Tervetuloa", + "year": "Vuosi" +} diff --git a/kirby/i18n/translations/fr.json b/kirby/i18n/translations/fr.json new file mode 100755 index 0000000..3c76d1c --- /dev/null +++ b/kirby/i18n/translations/fr.json @@ -0,0 +1,481 @@ +{ + "add": "Ajouter", + "avatar": "Image du profil", + "back": "Retour", + "cancel": "Annuler", + "change": "Changer", + "close": "Fermer", + "confirm": "Ok", + "copy": "Copier", + "create": "Créer", + + "date": "Date", + "date.select": "Choisissez une date", + + "day": "Jour", + "days.fri": "Ven", + "days.mon": "Lun", + "days.sat": "Sam", + "days.sun": "Dim", + "days.thu": "Jeu", + "days.tue": "Mar", + "days.wed": "Mer", + + "delete": "Supprimer", + "dimensions": "Dimensions", + "disabled": "Désactivé", + "discard": "Supprimer", + "download": "Télécharger", + "duplicate": "Dupliquer", + "edit": "Éditer", + + "dialog.files.empty": "Aucun fichier à sélectionner", + "dialog.pages.empty": "Aucune page à sélectionner", + "dialog.users.empty": "Aucun utilisateur à sélectionner", + + "email": "Courriel", + "email.placeholder": "mail@example.com", + + "error.access.login": "Identifiant incorrect", + "error.access.panel": "Vous n’êtes pas autorisé à accéder au Panel", + "error.access.view": "Vous n’êtes pas autorisé à accéder à cette section du Panel", + + "error.avatar.create.fail": "L’image du profil n’a pu être transférée", + "error.avatar.delete.fail": "L’image du profil n’a pu être supprimée", + "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.email.preset.notFound": "La configuration de courriel « {name} » n’a pu être trouvé", + + "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.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.invalid": "Type de média invalide : {mime}", + "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.type.forbidden": "Vous n’êtes pas autorisé à transférer des fichiers {type}", + "error.file.undefined": "Le fichier n’a pu être trouvé", + + "error.form.incomplete": "Veuillez corriger toutes les erreurs du formulaire…", + "error.form.notSaved": "Le formulaire n’a pu être enregistré", + + "error.language.code": "Veuillez saisir un code valide pour cette langue", + "error.language.duplicate": "Cette langue existe déjà", + "error.language.name": "Veuillez saisir un nom valide pour cette langue", + + "error.license.format": "Veuillez saisir un numéro de licence valide", + "error.license.email": "Veuillez saisir un courriel valide", + "error.license.verification": "La licence n’a pu être vérifiée", + + "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.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.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.num.invalid": + "Veuillez saisir un numéro de position valide. Les numéros ne doivent pas être négatifs.", + "error.page.slug.invalid": "Veuillez saisir un préfixe d’URL valide", + "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.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.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", + "error.site.update.permission": "Vous n’êtes pas autorisé à modifier le contenu global du site", + + "error.template.default.notFound": "Le modèle par défaut n’existe pas", + + "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.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.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.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.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.role.invalid": "Veuillez saisir un rôle valide", + "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.boolean": "Veuillez confirmer ou refuser", + "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.email": "Veuillez saisir un courriel valide", + "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", + "error.validation.ip": "Veuillez saisir une adresse IP valide", + "error.validation.less": "Veuillez saisir une valeur inférieure à {max}", + "error.validation.match": "La valeur ne correspond pas au modèle attendu", + "error.validation.max": "Veuillez saisir une valeur inférieure ou égale à {max}", + "error.validation.maxlength": + "Veuillez saisir une valeur plus courte (max. {max} caractères)", + "error.validation.maxwords": "Veuillez ne pas saisir plus de {max} mot(s)", + "error.validation.min": "Veuillez saisir une valeur supérieure ou égale à {min}", + "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.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.time": "Veuillez saisir une heure valide", + "error.validation.url": "Veuillez saisir une URL valide", + + "field.required": "Le champ est obligatoire", + "field.files.empty": "Pas encore de fichier sélectionné", + "field.pages.empty": "Pas encore de page sélectionnée", + "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.delete.confirm": + "Voulez-vous vraiment supprimer
{filename} ?", + + "files": "Fichiers", + "files.empty": "Pas encore de fichier", + + "hour": "Heure", + "insert": "Insérer", + "install": "Installer", + + "installation": "Installation", + "installation.completed": "Le Panel a été installé", + "installation.disabled": "L'installation du Panel est désactivée par défaut sur les serveurs publics. Veuillez lancer l'installation sur un serveur local, ou activez-la avec l'option panel.install.", + "installation.issues.accounts": + "Le dossier /site/accounts n’existe pas ou n’est pas accessible en écriture", + "installation.issues.content": + "Le dossier /content n’existe pas ou n’est pas accessible en écriture", + "installation.issues.curl": "L’extension CURL est requise", + "installation.issues.headline": "Le Panel ne peut être installé", + "installation.issues.mbstring": + "L’extension MB String est requise", + "installation.issues.media": + "Le dossier /media n’existe pas ou n’est pas accessible en écriture", + "installation.issues.php": "Veuillez utiliser PHP 7+", + "installation.issues.server": + "Kirby requiert Apache, Nginx ou Caddy", + "installation.issues.sessions": "Le dossier /site/sessions n’existe pas ou n’est pas accessible en écriture", + + "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.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.deleted": "La langue a été supprimée", + "language.direction": "Sens de lecture", + "language.direction.ltr": "De gauche à droite", + "language.direction.rtl": "De droite à gauche", + "language.locale": "Locales PHP", + "language.locale.warning": "Vous utilisez une Locale PHP personnalisée. Veuillez la modifier dans le fichier de langue situé dans /site/languages", + "language.name": "Nom", + "language.updated": "La langue a été mise à jour", + + "languages": "Langages", + "languages.default": "Langue par défaut", + "languages.empty": "Il n’y a pas encore de langues", + "languages.secondary": "Langues secondaires", + "languages.secondary.empty": "Il n’y a pas encore de langues secondaires", + + "license": "Licence", + "license.buy": "Acheter une licence", + "license.register": "S’enregistrer", + "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", + + "link": "Lien", + "link.text": "Texte du lien", + + "loading": "Chargement", + + "lock.unsaved": "Modifications non enregistrées", + "lock.unsaved.empty": "Il n’y a plus de modifications non enregistrées", + "lock.isLocked": "Modifications non enregistrées par {email}", + "lock.file.isLocked": "Le fichier est actuellement édité par {email} et ne peut être modifié.", + "lock.page.isLocked": "La page est actuellement éditée par {email} et ne peut être modifiée.", + "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.remember": "Rester connecté", + + "logout": "Se déconnecter", + + "menu": "Menu", + "meridiem": "AM/PM", + "mime": "Type de médias", + "minutes": "Minutes", + + "month": "Mois", + "months.april": "Avril", + "months.august": "Août", + "months.december": "Décembre", + "months.february": "Février", + "months.january": "Janvier", + "months.july": "Juillet", + "months.june": "Juin", + "months.march": "Mars", + "months.may": "Mai", + "months.november": "Novembre", + "months.october": "Octobre", + "months.september": "Septembre", + + "more": "Plus", + "name": "Nom", + "next": "Suivant", + "off": "off", + "on": "on", + "open": "Ouvrir", + "options": "Options", + + "orientation": "Orientation", + "orientation.landscape": "Paysage", + "orientation.portrait": "Portrait", + "orientation.square": "Carré", + + "page.changeSlug": "Modifier l’URL", + "page.changeSlug.fromTitle": "Créer à partir du titre", + "page.changeStatus": "Changer le statut", + "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.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", + "page.duplicate.appendix": "Copier", + "page.duplicate.files": "Copier les fichiers", + "page.duplicate.pages": "Copier les pages", + "page.status": "Statut", + "page.status.draft": "Brouillon", + "page.status.draft.description": + "La page est en mode brouillon et n’est visible que par les éditeurs connectés", + "page.status.listed": "Public", + "page.status.listed.description": "La page est publique pour tout le monde", + "page.status.unlisted": "Non listé", + "page.status.unlisted.description": "La page est uniquement accessible par son URL", + + "pages": "Pages", + "pages.empty": "Pas encore de pages", + "pages.status.draft": "Brouillons", + "pages.status.listed": "Publié", + "pages.status.unlisted": "Non listé", + + "pagination.page": "Page", + + "password": "Mot de passe", + "pixel": "Pixel", + "prev": "Précédent", + "remove": "Supprimer", + "rename": "Renommer", + "replace": "Remplacer", + "retry": "Essayer à nouveau", + "revert": "Revenir", + + "role": "Rôle", + "role.admin.description": "L’administrateur dispose de tous les droits", + "role.admin.title": "Administrateur", + "role.all": "Tous", + "role.empty": "Il n’y a aucun utilisateur avec ce rôle", + "role.description.placeholder": "Pas de description", + "role.nobody.description": "Ceci est un rôle de secours sans aucune permission.", + "role.nobody.title": "Personne", + + "save": "Enregistrer", + "search": "Rechercher", + + "section.required": "Cette section est obligatoire", + + "select": "Sélectionner", + "settings": "Paramètres", + "size": "Poids", + "slug": "Identifiant de l’URL", + "sort": "Trier", + "title": "Titre", + "template": "Modèle", + "today": "Aujourd’hui", + + "toolbar.button.code": "Code", + "toolbar.button.bold": "Gras", + "toolbar.button.email": "Courriel", + "toolbar.button.headings": "Titres", + "toolbar.button.heading.1": "Titre 1", + "toolbar.button.heading.2": "Titre 2", + "toolbar.button.heading.3": "Titre 3", + "toolbar.button.italic": "Italique", + "toolbar.button.file": "Fichier", + "toolbar.button.file.select": "Sélectionner un fichier", + "toolbar.button.file.upload": "Transférer un fichier", + "toolbar.button.link": "Lien", + "toolbar.button.ol": "Liste ordonnée", + "toolbar.button.ul": "Liste non-ordonnée", + + "translation.author": "Kirby Team", + "translation.direction": "ltr", + "translation.name": "Français", + "translation.locale": "fr_FR", + + "upload": "Transférer", + "upload.error.cantMove": "Le fichier transféré n’a pu être déplacé", + "upload.error.cantWrite": "Le fichier n’a pu être écrit sur le disque", + "upload.error.default": "Le fichier n’a pu être transféré", + "upload.error.extension": "Le transfert de fichier a été stoppé par une extension", + "upload.error.formSize": "Le fichier transféré excède la directive MAX_FILE_SIZE spécifiée dans le formulaire", + "upload.error.iniPostSize": "Le fichier transféré excède la directive post_max_size spécifiée dans php.ini", + "upload.error.iniSize": "Le fichier transféré excède la directive upload_max_filesize spécifiée dans php.ini", + "upload.error.noFile": "Aucun fichier n’a été transféré", + "upload.error.noFiles": "Aucun fichier n’a été transféré", + "upload.error.partial": "Le fichier n’a été que partiellement transféré", + "upload.error.tmpDir": "Un dossier temporaire est manquant", + "upload.errors": "Erreur", + "upload.progress": "Transfert en cours…", + + "url": "Url", + "url.placeholder": "https://example.com", + + "user": "Utilisateur", + "user.blueprint": + "Vous pouvez définir des sections et des champs de formulaire supplémentaires pour ce rôle d’utilisateur dans /site/blueprints/users/{role}.yml", + "user.changeEmail": "Modifier le courriel", + "user.changeLanguage": "Modifier la langue", + "user.changeName": "Renommer cet utilisateur", + "user.changePassword": "Modifier le mot de passe", + "user.changePassword.new": "Nouveau mot de passe", + "user.changePassword.new.confirm": "Confirmer le nouveau mot de passe…", + "user.changeRole": "Modifier le rôle", + "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}?", + + "users": "Utilisateurs", + + "version": "Version", + + "view.account": "Votre compte", + "view.installation": "Installation", + "view.settings": "Paramètres", + "view.site": "Site", + "view.users": "Utilisateurs", + + "welcome": "Bienvenue", + "year": "Année" +} diff --git a/kirby/i18n/translations/hu.json b/kirby/i18n/translations/hu.json new file mode 100755 index 0000000..2526278 --- /dev/null +++ b/kirby/i18n/translations/hu.json @@ -0,0 +1,481 @@ +{ + "add": "Hozz\u00e1ad", + "avatar": "Profilkép", + "back": "Vissza", + "cancel": "M\u00e9gsem", + "change": "M\u00f3dos\u00edt\u00e1s", + "close": "Bez\u00e1r", + "confirm": "Mentés", + "copy": "Másol", + "create": "Létrehoz", + + "date": "Dátum", + "date.select": "Dátum kiválasztása", + + "day": "Nap", + "days.fri": "p\u00e9", + "days.mon": "h\u00e9", + "days.sat": "szo", + "days.sun": "va", + "days.thu": "cs\u00fc", + "days.tue": "ke", + "days.wed": "sze", + + "delete": "T\u00f6rl\u00e9s", + "dimensions": "Méretek", + "disabled": "Disabled", + "discard": "Visszavon\u00e1s", + "download": "Letöltés", + "duplicate": "Másolat", + "edit": "Aloldal szerkeszt\u00e9se", + + "dialog.files.empty": "Nincsenek fájlok kiválasztva", + "dialog.pages.empty": "Nincsenek oldalak kiválasztva", + "dialog.users.empty": "Nincsenek felhasználók kiválasztva", + + "email": "Email", + "email.placeholder": "mail@pelda.hu", + + "error.access.login": "Érvénytelen bejelentkezés", + "error.access.panel": "Nincs jogosultságod megnyitni a panelt", + "error.access.view": "You are not allowed to access this part of the panel", + + "error.avatar.create.fail": "A profilkép feltöltése nem sikerült", + "error.avatar.delete.fail": "A profilkép nem törölhető", + "error.avatar.dimensions.invalid": + "A profilkép maximális szélessége és magassága 3000 pixel lehet", + "error.avatar.mime.forbidden": + "A profilkép formátuma csak JPEG vagy PNG lehet", + + "error.blueprint.notFound": "A \"{name}\" oldalsablon nem tölthető be", + + "error.email.preset.notFound": "A \"{name}\" email-beállítás nem található", + + "error.field.converter.invalid": "Érvénytelen konverter: \"{converter}\"", + + "error.file.changeName.empty": "The name must not be empty", + "error.file.changeName.permission": + "Nincs jogosultságod megváltoztatni a \"{filename}\" fájl nevét", + "error.file.duplicate": "Már létezik \"{filename}\" nevű fájl", + "error.file.extension.forbidden": + "Tiltott kiterjeszt\u00e9s\u0171 f\u00e1jl", + "error.file.extension.missing": + "Kiterjeszt\u00e9s n\u00e9lk\u00fcli f\u00e1jl nem t\u00f6lthet\u0151 fel", + "error.file.maxheight": "The height of the image must not exceed {height} pixels", + "error.file.maxsize": "The file is too large", + "error.file.maxwidth": "The width of the image must not exceed {width} pixels", + "error.file.mime.differs": + "A feltöltött fájlnak azonos \"{mime}\" típusúnak kell lennie", + "error.file.mime.forbidden": "A \"{mime}\" típusú médiafájlok nem engedélyezettek", + "error.file.mime.invalid": "Invalid mime type: {mime}", + "error.file.mime.missing": + "A \"{filename}\" fájl típusa nem állapítható meg", + "error.file.minheight": "The height of the image must be at least {height} pixels", + "error.file.minsize": "The file is too small", + "error.file.minwidth": "The width of the image must be at least {width} pixels", + "error.file.name.missing": "A fálj neve nem lehet üres", + "error.file.notFound": "A \"{filename}\" fájl nem található", + "error.file.orientation": "The orientation of the image must be \"{orientation}\"", + "error.file.type.forbidden": "Nem tölthetsz fel \"{type}\" típusú fájlokat", + "error.file.undefined": "A f\u00e1jl nem tal\u00e1lhat\u00f3", + + "error.form.incomplete": "Kérlek javítsd ki az összes hibát az űrlapon", + "error.form.notSaved": "Az űrlap nem menthető", + + "error.language.code": "Kérlek, add meg a nyelv érvényes kódját", + "error.language.duplicate": "A nyelv már létezik", + "error.language.name": "Kérlek, add meg a nyelv érvényes nevét", + + "error.license.format": "Kérlek, add meg az évényes lincensz kulcsot", + "error.license.email": "Kérlek adj meg egy valós email-címet", + "error.license.verification": "A licensz nem ellenőrizhető", + + "error.page.changeSlug.permission": + "Nem változtathatod meg az URL-előtagot: \"{slug}\"", + "error.page.changeStatus.incomplete": + "Az oldal hibákat tartalmaz és nem publikálható", + "error.page.changeStatus.permission": + "Az oldal státusza nem változtatható meg", + "error.page.changeStatus.toDraft.invalid": + "A(z) \"{slug}\" oldalt nem lehet piszkozattá alakítani", + "error.page.changeTemplate.invalid": + "A \"{slug}\" oldal sablonját nem lehet megváltoztatni", + "error.page.changeTemplate.permission": + "Nincs jogosultságod megváltoztatni a sablont ehhez: \"{slug}\"", + "error.page.changeTitle.empty": "A cím nem lehet üres", + "error.page.changeTitle.permission": + "Nincs jogosultságod megváltoztatni a címet: \"{slug}\"", + "error.page.create.permission": "Nincs jogosultságod az oldal létrehozásához: \"{slug}\"", + "error.page.delete": "A(z) \"{slug}\" oldal nem törölhető", + "error.page.delete.confirm": "Megerősítéshez add meg az oldal címét", + "error.page.delete.hasChildren": + "Az oldalnak vannak aloldalai és nem törölhető", + "error.page.delete.permission": "Nincs jogosultságod a(z) \"{slug}\" oldal törléséhez", + "error.page.draft.duplicate": + "Van már egy másik oldal ezzel az URL-lel: \"{slug}\"", + "error.page.duplicate": + "Van már egy másik oldal ezzel az URL-lel: \"{slug}\"", + "error.page.duplicate.permission": "Nincs engedélyed a(z) \"{slug}\" másolat keszítéséhez", + "error.page.notFound": "Az oldal nem tal\u00e1lhat\u00f3", + "error.page.num.invalid": + "Kérlek megfelelő oldalszámozást adj meg. Negatív szám itt nem használható.", + "error.page.slug.invalid": "Kérlek megfelelő URL-előtagot adj meg", + "error.page.sort.permission": "A(z) \"{slug}\" oldal nem illeszthető a sorrendbe", + "error.page.status.invalid": "Kérlek add meg a megfelelő oldalstátuszt", + "error.page.undefined": "Az oldal nem tal\u00e1lhat\u00f3", + "error.page.update.permission": "Nincs jogosultságod a(z) \"{slug}\" oldal frissítéséhez", + + "error.section.files.max.plural": + "Maximum {max} fájlt adhatsz hozzá a(z) \"{section}\" szekcióhoz", + "error.section.files.max.singular": + "Nem adhatsz hozzá egynél több fájlt a(z) \"{section}\" szekcióhoz", + "error.section.files.min.plural": + "A \"{section}\" szakasz legalább {min} fájlt igényel", + "error.section.files.min.singular": + "A \"{section}\" szakasz legalább egy fájlt igényel", + + "error.section.pages.max.plural": + "Maximum {max} oldalt adhatsz hozzá a(z) \"{section}\" szekcióhoz", + "error.section.pages.max.singular": + "Nem adhatsz hozzá egynél több oldalt a(z) \"{section}\" szekcióhoz", + "error.section.pages.min.plural": + "A \"{section}\" szakasz legalább {min} oldalt igényel", + "error.section.pages.min.singular": + "A \"{section}\" szakasz legalább egy oldalt igényel", + + "error.section.notLoaded": "A(z) \"{name}\" szekció nem tölthető be", + "error.section.type.invalid": "A szekció típusa (\"{type}\") nem megfelelő", + + "error.site.changeTitle.empty": "A cím nem lehet üres", + "error.site.changeTitle.permission": + "Nincs jogosultságod megváltoztatni az honlap címét", + "error.site.update.permission": "Nincs jogosultságod frissíteni a honlapot", + + "error.template.default.notFound": "Az alapértelmezett sablon nem létezik", + + "error.user.changeEmail.permission": + "Nincs jogosultságod megváltoztatni \"{name}\" felhasználó email-címét", + "error.user.changeLanguage.permission": + "Nincs jogosultságod megváltoztatni \"{name}\" felhasználó nyelvi beállításait", + "error.user.changeName.permission": + "Nincs jogosultságod megváltoztatni \"{name}\" felhasználó nevét", + "error.user.changePassword.permission": + "Nincs jogosultságod megváltoztatni \"{name}\" felhasználó jelszavát", + "error.user.changeRole.lastAdmin": + "Az egyedüli adminisztrátor szerepkörét nem lehet megváltoztatni", + "error.user.changeRole.permission": + "Nincs jogosultságod megváltoztatni \"{name}\" felhasználó szerepkörét", + "error.user.changeRole.toAdmin": + "You are not allowed to promote someone to the admin role", + "error.user.create.permission": "Nincs jogosultságod létrehozni ezt a felhasználót", + "error.user.delete": "A felhaszn\u00e1l\u00f3 nem t\u00f6r\u00f6lhet\u0151", + "error.user.delete.lastAdmin": "Nem t\u00f6r\u00f6lheted az egyetlen adminisztr\u00e1tort", + "error.user.delete.lastUser": "Nem törölheted az egyetlen felhasználót", + "error.user.delete.permission": + "Nincs jogosults\u00e1god t\u00f6r\u00f6lni ezt a felhaszn\u00e1l\u00f3t", + "error.user.duplicate": + "Már létezik felhasználó \"{email}\" email-címmel", + "error.user.email.invalid": "Kérlek adj meg egy valós email-címet", + "error.user.language.invalid": "Kérlek add meg a megfelelő nyelvi beállítást", + "error.user.notFound": "A felhaszn\u00e1l\u00f3 nem tal\u00e1lhat\u00f3", + "error.user.password.invalid": + "Kérlek adj meg egy megfelelő jelszót. A jelszónak legalább 8 karakter hosszúságúnak kell lennie.", + "error.user.password.notSame": "K\u00e9rlek er\u0151s\u00edtsd meg a jelsz\u00f3t", + "error.user.password.undefined": "A felhasználónak nincs jelszó megadva", + "error.user.role.invalid": "Kérlek adj meg egy megfelelő szerepkört", + "error.user.update.permission": + "Nincs jogosultságod frissíteni \"{name}\" felhasználó adatait", + + "error.validation.accepted": "Kérlek erősítsd meg", + "error.validation.alpha": "Kérlek csak kis betűket használj (a-z)", + "error.validation.alphanum": + "Kérlek csak kis betűket és számjegyeket használj (a-z, 0-9)", + "error.validation.between": + "Kérlek egy \"{min}\" és \"{max}\" közötti értéket adj meg", + "error.validation.boolean": "Kérlek erősítsd meg vagy vesd el", + "error.validation.contains": + "Kérlek olyan értéket adj meg, amely tartalmazza ezt: \"{needle}\"", + "error.validation.date": "Kérlek megfelelő dátumot adj meg", + "error.validation.date.after": "Please enter a date after {date}", + "error.validation.date.before": "Please enter a date before {date}", + "error.validation.date.between": "Please enter a date between {min} and {max}", + "error.validation.denied": "Kérlek vesd el", + "error.validation.different": "Az érték nem lehet \"{other}\"", + "error.validation.email": "Kérlek adj meg egy valós email-címet", + "error.validation.endswith": "Az értéknek erre kell végződnie: \"{end}\"", + "error.validation.filename": "Kérlek megfelelő fájlnevet adj meg", + "error.validation.in": "Kérlek adj meg egyet az alábbiak közül: ({in})", + "error.validation.integer": "Kérlek valós számot adj meg", + "error.validation.ip": "Kérlek megfelelő IP-címet adj meg", + "error.validation.less": "A megadott érték kevesebb legyen, mint {max}", + "error.validation.match": "A megadott érték nem felel meg az elvárt struktúrának", + "error.validation.max": "A megadott érték egyenlő vagy kevesebb legyen, mint {max}", + "error.validation.maxlength": + "Kérlek rövidebb értéket adj meg (legfeljebb {max} karakter)", + "error.validation.maxwords": "Kérlek ide legfeljebb {max} szót írj", + "error.validation.min": "A megadott érték egyenlő vagy nagyobb legyen, mint {min}", + "error.validation.minlength": + "Kérlek hosszabb értéket adj meg (legalább {min} karakter)", + "error.validation.minwords": "Kérlek ide legalább {min} szót írj", + "error.validation.more": "A megadott érték legyen nagyobb, mint {min} ", + "error.validation.notcontains": + "Kérlek olyan értéket adj meg, amely nem tartalmazza ezt: \"{needle}\" ", + "error.validation.notin": + "Kérlek egyiket se használd az alábbiak közül: ({notIn})", + "error.validation.option": "Kérlek válassz egy megfelelő opciót", + "error.validation.num": "Kérlek adj meg egy megfelelő számot", + "error.validation.required": "Kérlek írj be valamit", + "error.validation.same": "Kérlek írd be: \"{other}\"", + "error.validation.size": "Az értéknek az alábbi méretűnek kell lennie: \"{size}\"", + "error.validation.startswith": "Az értéknek ezzel kell kezdődnie: \"{start}\"", + "error.validation.time": "Kérlek megfelelő időt adj meg", + "error.validation.url": "Kérlek megfelelő URL-t adj meg", + + "field.required": "The field is required", + "field.files.empty": "Nincs fálj kiválasztva", + "field.pages.empty": "Nincs oldal kiválasztva", + "field.structure.delete.confirm": "Biztos t\u00f6r\u00f6lni szeretn\u00e9d ezt a bejegyz\u00e9st?", + "field.structure.empty": "Nincs m\u00e9g bejegyz\u00e9s", + "field.users.empty": "Nincs felhasználó kiválasztva", + + "file.delete.confirm": + "Biztos törölni akarod ezt a fájlt:
{filename}?", + + "files": "Fájlok", + "files.empty": "Még nincsenek fájlok", + + "hour": "Óra", + "insert": "Beilleszt", + "install": "Telepítés", + + "installation": "Telepítés", + "installation.completed": "A panel sikeresen telepítve", + "installation.disabled": "A panel telepítője alapértelmezés szerint le van tiltva a nyilvános szervereken. Kérlek, futtassd a telepítőt egy helyi gépen vagy engedélyezze a panel.install opcióval.", + "installation.issues.accounts": + "A /site/accounts mappa nem létezik, vagy nem írható", + "installation.issues.content": + "A /content mappa nem létezik vagy nem írható", + "installation.issues.curl": "A CURL bővítmény engedélyezése szükséges", + "installation.issues.headline": "A panel telepítése sikertelen", + "installation.issues.mbstring": + "Az MB String bővítmény engedélyezése szükséges", + "installation.issues.media": + "A /media mappa nem létezik vagy nem írható", + "installation.issues.php": "Bizonyosodj meg róla, hogy az általad használt PHP-verzió PHP 7+", + "installation.issues.server": + "A Kirby az alábbi szervereken futtatható: Apache, Nginx vagy Caddy", + "installation.issues.sessions": "A /site/sessions könyvtár nem létezik vagy nem írható", + + "language": "Nyelv", + "language.code": "Kód", + "language.convert": "Alapértelmezettnek jelölés", + "language.convert.confirm": + "

Tényleg az alaőértelmezett nyelvre szeretnéd konvertálni ezt: {name}? Ez a művelet nem vonható vissza.

Ha{name} olyat is tartalmaz, amelynek nincs megfelelő fordítása, a honlapod egyes részei az új alapértelmezett nyelv hiányosságai miatt üresek maradhatnak.

", + "language.create": "Új nyelv hozzáadása", + "language.delete.confirm": + "Tényleg törölni szeretnéd a(z) {name} nyelvet, annak minden fordításával együtt? Ez a művelet nem vonható vissza!", + "language.deleted": "A nyelv törölve lett", + "language.direction": "Olvasási irány", + "language.direction.ltr": "Balról jobbra", + "language.direction.rtl": "Jobbról balra", + "language.locale": "PHP locale sztring", + "language.locale.warning": "You are using a custom locale set up. Please modify it in the language file in /site/languages", + "language.name": "Név", + "language.updated": "A nyelv frissítve lett", + + "languages": "Nyelvek", + "languages.default": "Alapértelmezett nyelv", + "languages.empty": "Nincsnek még nyelvek", + "languages.secondary": "Másodlagos nyelvek", + "languages.secondary.empty": "Nincsnek még másodlagos nyelvek", + + "license": "Kirby licenc", + "license.buy": "Licenc vásárlása", + "license.register": "Regisztráció", + "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", + + "link": "Link", + "link.text": "Link szövege", + + "loading": "Betöltés", + + "lock.unsaved": "Nem mentett változások", + "lock.unsaved.empty": "There are no more unsaved changes", + "lock.isLocked": "Nem mentett {email} változások", + "lock.file.isLocked": "A fájlt jelenleg {email} szerkeszti és nem módosítható.", + "lock.page.isLocked": "Az oldalt jelenleg {email} szerkeszti és nem módosítható.", + "lock.unlock": "Kinyit", + "lock.isUnlocked": "A nem mentett módosításokat egy másik felhasználó felülírta. A módosításokat manuálisan egyesítheted.", + + "login": "Bejelentkezés", + "login.remember": "Maradjak bejelentkezve", + + "logout": "Kijelentkezés", + + "menu": "Menü", + "meridiem": "DE/DU", + "mime": "Média-típus", + "minutes": "Perc", + + "month": "Hónap", + "months.april": "\u00e1prilis", + "months.august": "augusztus", + "months.december": "december", + "months.february": "febru\u00e1r", + "months.january": "janu\u00e1r", + "months.july": "j\u00falius", + "months.june": "j\u00fanius", + "months.march": "m\u00e1rcius", + "months.may": "m\u00e1jus", + "months.november": "november", + "months.october": "okt\u00f3ber", + "months.september": "szeptember", + + "more": "Több", + "name": "Név", + "next": "Következő", + "off": "ki", + "on": "be", + "open": "Megnyitás", + "options": "Beállítások", + + "orientation": "Tájolás", + "orientation.landscape": "Fekvő", + "orientation.portrait": "Álló", + "orientation.square": "Négyzetes", + + "page.changeSlug": "URL v\u00e1ltoztat\u00e1sa", + "page.changeSlug.fromTitle": "L\u00e9trehoz\u00e1s c\u00edmb\u0151l", + "page.changeStatus": "Állapot módosítása", + "page.changeStatus.position": "Kérlek válaszd ki a pozíciót", + "page.changeStatus.select": "Új állapot kiválasztása", + "page.changeTemplate": "Sablon módosítása", + "page.delete.confirm": + "Biztos vagy benne, hogy törlöd az alábbi oldalt: {title}?", + "page.delete.confirm.subpages": + "Ehhez az oldalhoz aloldalak tartoznak.
Az oldal törlésekor a hozzá tartozó aloldalak is törlődnek.", + "page.delete.confirm.title": "Megerősítéshez add meg az oldal címét", + "page.draft.create": "Piszkozat létrehozása", + "page.duplicate.appendix": "Másol", + "page.duplicate.files": "Fájlok másolása", + "page.duplicate.pages": "Oldalak másolása", + "page.status": "Állapot", + "page.status.draft": "Piszkozat", + "page.status.draft.description": + "Az oldal jelenleg piszkozat státuszban van és csak bejelentkezett szerkesztők számára látható", + "page.status.listed": "Publikus", + "page.status.listed.description": "Az oldal mindenki számára elérhető", + "page.status.unlisted": "Nem listázott", + "page.status.unlisted.description": "Az oldal csak URL-en keresztül érhető el", + + "pages": "Oldalak", + "pages.empty": "Nincs még bejegyzés", + "pages.status.draft": "Piszkozatok", + "pages.status.listed": "Publikálva", + "pages.status.unlisted": "Nem listázott", + + "pagination.page": "Oldal", + + "password": "Jelsz\u00f3", + "pixel": "Pixel", + "prev": "Előző", + "remove": "Eltávolítás", + "rename": "Átnevezés", + "replace": "Cser\u00e9l", + "retry": "Próbáld újra", + "revert": "Visszavon\u00e1s", + + "role": "Szerepkör", + "role.admin.description": "Az adminisztrátornak minden joga van", + "role.admin.title": "Admin", + "role.all": "Összes", + "role.empty": "Nincsenek felhasználók ilyen szerepkörrel", + "role.description.placeholder": "Nincs leírás", + "role.nobody.description": "Ez a visszatérő szabály a nem rendelkező jogosultsághoz", + "role.nobody.title": "Senki", + + "save": "Ment\u00e9s", + "search": "Keresés", + + "section.required": "The section is required", + + "select": "Kiválasztás", + "settings": "Beállítások", + "size": "Méret", + "slug": "URL n\u00e9v", + "sort": "Rendezés", + "title": "Cím", + "template": "Sablon", + "today": "Ma", + + "toolbar.button.code": "Kód", + "toolbar.button.bold": "F\u00e9lk\u00f6v\u00e9r sz\u00f6veg", + "toolbar.button.email": "Email", + "toolbar.button.headings": "Cím", + "toolbar.button.heading.1": "Cím 1", + "toolbar.button.heading.2": "Cím 2", + "toolbar.button.heading.3": "Cím 3", + "toolbar.button.italic": "Dőlt szöveg", + "toolbar.button.file": "Fájl", + "toolbar.button.file.select": "Válassz egy fájlt", + "toolbar.button.file.upload": "Fájl feltöltése", + "toolbar.button.link": "Link", + "toolbar.button.ol": "Rendezett lista", + "toolbar.button.ul": "Rendezetlen lista", + + "translation.author": "A Kirby csapata", + "translation.direction": "ltr", + "translation.name": "Magyar", + "translation.locale": "hu_HU", + + "upload": "Feltöltés", + "upload.error.cantMove": "The uploaded file could not be moved", + "upload.error.cantWrite": "Failed to write file to disk", + "upload.error.default": "The file could not be uploaded", + "upload.error.extension": "File upload stopped by extension", + "upload.error.formSize": "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the form", + "upload.error.iniPostSize": "The uploaded file exceeds the post_max_size directive in php.ini", + "upload.error.iniSize": "The uploaded file exceeds the upload_max_filesize directive in php.ini", + "upload.error.noFile": "No file was uploaded", + "upload.error.noFiles": "No files were uploaded", + "upload.error.partial": "The uploaded file was only partially uploaded", + "upload.error.tmpDir": "Missing a temporary folder", + "upload.errors": "Hiba", + "upload.progress": "Feltöltés...", + + "url": "Url", + "url.placeholder": "https://pelda.hu", + + "user": "Felhasználó", + "user.blueprint": + "Ehhez a szerepkörhöz további szekciókat és mezőket vehetsz fel a /site/blueprints/users/{role}.yml fájlban", + "user.changeEmail": "Email módosítása", + "user.changeLanguage": "Nyelv módosítása", + "user.changeName": "Felhasználó átnevezése", + "user.changePassword": "Jelszó módosítása", + "user.changePassword.new": "Új jelszó", + "user.changePassword.new.confirm": "Az új jelszó megerősítése", + "user.changeRole": "Szerepkör módosítása", + "user.changeRole.select": "Új szerepkör kiválasztása", + "user.create": "Új felhasználó hozzáadása", + "user.delete": "Felhasználó törlése", + "user.delete.confirm": + "Biztos törlöd ezt a felhasználót:
{email}?", + + "users": "Felhasználók", + + "version": "Kirby verzi\u00f3", + + "view.account": "Fi\u00f3kod", + "view.installation": "Telep\u00edt\u00e9s", + "view.settings": "Beállítások", + "view.site": "Weboldal", + "view.users": "Felhaszn\u00e1l\u00f3k", + + "welcome": "Üdvözlünk", + "year": "Év" +} diff --git a/kirby/i18n/translations/id.json b/kirby/i18n/translations/id.json new file mode 100755 index 0000000..01a783a --- /dev/null +++ b/kirby/i18n/translations/id.json @@ -0,0 +1,481 @@ +{ + "add": "Tambah", + "avatar": "Gambar profil", + "back": "Kembali", + "cancel": "Batal", + "change": "Ubah", + "close": "Tutup", + "confirm": "Oke", + "copy": "Salin", + "create": "Buat", + + "date": "Tanggal", + "date.select": "Pilih tanggal", + + "day": "Hari", + "days.fri": "Jum", + "days.mon": "Sen", + "days.sat": "Sab", + "days.sun": "Min", + "days.thu": "Kam", + "days.tue": "Sel", + "days.wed": "Rab", + + "delete": "Hapus", + "dimensions": "Dimensi", + "disabled": "Dimatikan", + "discard": "Buang", + "download": "Unduh", + "duplicate": "Duplikasi", + "edit": "Sunting", + + "dialog.files.empty": "Tidak ada berkas untuk dipilih", + "dialog.pages.empty": "Tidak ada halaman untuk dipilih", + "dialog.users.empty": "Tidak ada pengguna untuk dipilih", + + "email": "Surel", + "email.placeholder": "surel@contoh.com", + + "error.access.login": "Upaya masuk tidak valid", + "error.access.panel": "Anda tidak diizinkan mengakses panel", + "error.access.view": "Anda tidak diizinkan mengakses bagian panel ini", + + "error.avatar.create.fail": "Gambar profil tidak dapat diunggah", + "error.avatar.delete.fail": "Gambar profil tidak dapat dihapus", + "error.avatar.dimensions.invalid": + "Pastikan lebar dan tinggi gambar profil di bawah 3000 piksel", + "error.avatar.mime.forbidden": + "Gambar profil harus berupa berkas JPEG atau PNG", + + "error.blueprint.notFound": "Cetak biru \"{name}\" tidak dapat dimuat", + + "error.email.preset.notFound": "Surel \"{name}\" tidak dapat ditemukan", + + "error.field.converter.invalid": "Konverter \"{converter}\" tidak valid", + + "error.file.changeName.empty": "Nama harus diisi", + "error.file.changeName.permission": + "Anda tidak diizinkan mengubah nama berkas \"{filename}\"", + "error.file.duplicate": "Berkas dengan nama \"{filename}\" sudah ada", + "error.file.extension.forbidden": + "Ekstensi \"{extension}\" tidak diizinkan", + "error.file.extension.missing": + "Berkas \"{filename}\" harus memiliki ekstensi", + "error.file.maxheight": "The height of the image must not exceed {height} pixels", + "error.file.maxsize": "The file is too large", + "error.file.maxwidth": "The width of the image must not exceed {width} pixels", + "error.file.mime.differs": + "Berkas yang diunggah harus memiliki tipe mime sama \"{mime}\"", + "error.file.mime.forbidden": "Media dengan tipe mime \"{mime}\" tidak diizinkan", + "error.file.mime.invalid": "Invalid mime type: {mime}", + "error.file.mime.missing": + "Tipe media untuk \"{filename}\" tidak dapat dideteksi", + "error.file.minheight": "The height of the image must be at least {height} pixels", + "error.file.minsize": "The file is too small", + "error.file.minwidth": "The width of the image must be at least {width} pixels", + "error.file.name.missing": "Nama berkas harus diisi", + "error.file.notFound": "Berkas \"{filename}\" tidak dapat ditemukan", + "error.file.orientation": "The orientation of the image must be \"{orientation}\"", + "error.file.type.forbidden": "Anda tidak diizinkan mengunggah berkas dengan tipe {type}", + "error.file.undefined": "Berkas tidak dapat ditemukan", + + "error.form.incomplete": "Pastikan semua bidang telah diisi dengan benar…", + "error.form.notSaved": "Formulir tidak dapat disimpan", + + "error.language.code": "Masukkan kode bahasa yang valid", + "error.language.duplicate": "Bahasa sudah ada", + "error.language.name": "Masukkan nama bahasa yang valid", + + "error.license.format": "Masukkan kode lisensi yang valid", + "error.license.email": "Masukkan surel yang valid", + "error.license.verification": "Lisensi tidak dapat diverifikasi", + + "error.page.changeSlug.permission": + "Anda tidak diizinkan mengubah akhiran URL untuk \"{slug}\"", + "error.page.changeStatus.incomplete": + "Halaman memiliki kesalahan dan tidak dapat diterbitkan", + "error.page.changeStatus.permission": + "Status halaman ini tidak dapat diubah", + "error.page.changeStatus.toDraft.invalid": + "Halaman \"{slug}\" tidak dapat dikonversi menjadi draf", + "error.page.changeTemplate.invalid": + "Templat untuk halaman \"{slug}\" tidak dapat diubah", + "error.page.changeTemplate.permission": + "Anda tidak diizinkan mengubah templat dari \"{slug}\"", + "error.page.changeTitle.empty": "Judul harus diisi", + "error.page.changeTitle.permission": + "Anda tidak diizinkan mengubah judul dari \"{slug}\"", + "error.page.create.permission": "Anda tidak diizinkan membuat \"{slug}\"", + "error.page.delete": "Halaman \"{slug}\" tidak dapat dihapus", + "error.page.delete.confirm": "Masukkan judul halaman untuk mengonfirmasi", + "error.page.delete.hasChildren": + "Halaman ini memiliki sub-halaman dan tidak dapat dihapus", + "error.page.delete.permission": "Anda tidak diizinkan menghapus \"{slug}\"", + "error.page.draft.duplicate": + "Draf halaman dengan akhiran URL \"{slug}\" sudah ada", + "error.page.duplicate": + "Halaman dengan akhiran URL \"{slug}\" sudah ada", + "error.page.duplicate.permission": "Anda tidak diizinkan menduplikasi \"{slug}\"", + "error.page.notFound": "Halaman \"{slug}\" tidak dapat ditemukan", + "error.page.num.invalid": + "Masukkan nomor urut yang valid. Nomor tidak boleh negatif.", + "error.page.slug.invalid": "Masukkan awalan URL yang valid", + "error.page.sort.permission": "Halaman \"{slug}\" tidak dapat diurutkan", + "error.page.status.invalid": "Atur status halaman yang valid", + "error.page.undefined": "Halaman tidak dapat ditemukan", + "error.page.update.permission": "Anda tidak diizinkan memperbaharui \"{slug}\"", + + "error.section.files.max.plural": + "Anda hanya boleh menambahkan maksimal {max} berkas ke bagian \"{section}\"", + "error.section.files.max.singular": + "Anda hanya boleh menambahkan satu berkas ke bagian \"{section}\"", + "error.section.files.min.plural": + "Bagian \"{section}\" setidaknya memiliki {min} berkas", + "error.section.files.min.singular": + "Bagian \"{section}\" setidaknya memiliki satu berkas", + + "error.section.pages.max.plural": + "Anda hanya boleh menambahkan maksimal {max} halaman ke bagian \"{section}\"", + "error.section.pages.max.singular": + "Anda hanya boleh menambahkan satu halaman ke bagian \"{section}\"", + "error.section.pages.min.plural": + "Bagian \"{section}\" setidaknya memiliki {min} halaman", + "error.section.pages.min.singular": + "Bagian \"{section}\" setidaknya memiliki satu halaman", + + "error.section.notLoaded": "Bagian \"{name}\" tidak dapat dimuat", + "error.section.type.invalid": "Tipe bagian \"{type}\" tidak valid", + + "error.site.changeTitle.empty": "Judul harus diisi", + "error.site.changeTitle.permission": + "Anda tidak diizinkan mengubah judul situs", + "error.site.update.permission": "Anda tidak diizinkan memperbaharui situs", + + "error.template.default.notFound": "Templat bawaan tidak ada", + + "error.user.changeEmail.permission": + "Anda tidak diizinkan mengubah surel dari pengguna \"{name}\"", + "error.user.changeLanguage.permission": + "Anda tidak diizinkan mengubah bahasa dari pengguna \"{name}\"", + "error.user.changeName.permission": + "Anda tidak diizinkan mengubah nama dari pengguna \"{name}\"", + "error.user.changePassword.permission": + "Anda tidak diizinkan mengubah sandi dari pengguna \"{name}\"", + "error.user.changeRole.lastAdmin": + "Peran dari admin satu-satunya tidak dapat diubah", + "error.user.changeRole.permission": + "Anda tidak diizinkan mengubah peran dari pengguna \"{name}\"", + "error.user.changeRole.toAdmin": + "Anda tidak diizinkan mempromosikan seseorang menjadi admin", + "error.user.create.permission": "Anda tidak diizinkan membuat pengguna ini", + "error.user.delete": "Pengguna \"{nama}\" tidak dapat dihapus", + "error.user.delete.lastAdmin": "Admin satu-satunya tidak dapat dihapus", + "error.user.delete.lastUser": "Pengguna satu-satunya tidak dapat dihapus", + "error.user.delete.permission": + "Anda tidak diizinkan menghapus pengguna \"{name}\"", + "error.user.duplicate": + "Pengguna dengan surel \"{email}\" sudah ada", + "error.user.email.invalid": "Masukkan surel yang valid", + "error.user.language.invalid": "Masukkan bahasa yang valid", + "error.user.notFound": "Pengguna \"{name}\" tidak dapat ditemukan", + "error.user.password.invalid": + "Masukkan sandi yang valid. Sandi setidaknya mengandung 8 karakter.", + "error.user.password.notSame": "Sandi tidak cocok", + "error.user.password.undefined": "Pengguna tidak memiliki sandi", + "error.user.role.invalid": "Masukkan peran yang valid", + "error.user.update.permission": + "Anda tidak diizinkan memperbaharui pengguna \"{name}\"", + + "error.validation.accepted": "Mohon konfirmasi", + "error.validation.alpha": "Masukkan hanya karakter a-z", + "error.validation.alphanum": + "Masukkan hanya karakter a-z atau 0-9", + "error.validation.between": + "Masukkan nilai antara \"{min}\" dan \"{max}\"", + "error.validation.boolean": "Mohon konfirmasi atau tolak", + "error.validation.contains": + "Masukkan nilai yang mengandung \"{needle}\"", + "error.validation.date": "Masukkan tanggal yang valid", + "error.validation.date.after": "Masukkan tanggal setelah {date}", + "error.validation.date.before": "Masukkan tanggal sebelum {date}", + "error.validation.date.between": "Masukkan tanggal antara {min} dan {max}", + "error.validation.denied": "Mohon tolak", + "error.validation.different": "Nilai harus selain \"{other}\"", + "error.validation.email": "Masukkan surel yang valid", + "error.validation.endswith": "Nilai harus diakhiri dengan \"{end}\"", + "error.validation.filename": "Masukkan nama berkas yang valid", + "error.validation.in": "Masukkan satu dari berikut: ({in})", + "error.validation.integer": "Masukkan bilangan bulat yang valid", + "error.validation.ip": "Masukkan IP yang valid", + "error.validation.less": "Masukkan nilai kurang dari {max}", + "error.validation.match": "Nilai tidak cocok dengan pola yang semestinya", + "error.validation.max": "Masukkan nilai yang sama dengan atau kurang dari {max}", + "error.validation.maxlength": + "Masukkan nilai yang lebih pendek. (maksimal {max} karakter)", + "error.validation.maxwords": "Masukkan tidak lebih dari {max} kata", + "error.validation.min": "Masukkan nilai yang sama dengan atau lebih dari {min}", + "error.validation.minlength": + "Masukkan nilai yang lebih panjang. (minimal {min} karakter)", + "error.validation.minwords": "Masukkan setidaknya {min} kata", + "error.validation.more": "Masukkan nilai yang lebih besar dari {min}", + "error.validation.notcontains": + "Masukkan nilai yang tidak mengandung \"{needle}\"", + "error.validation.notin": + "Jangan masukkan satupun: ({notIn})", + "error.validation.option": "Pilih opsi yang valid", + "error.validation.num": "Masukkan nomor yang valid", + "error.validation.required": "Masukkan sesuatu", + "error.validation.same": "Masukkan \"{other}\"", + "error.validation.size": "Ukuran dari nilai harus \"{size}\"", + "error.validation.startswith": "Nilai harus diawali dengan \"{start}\"", + "error.validation.time": "Masukkan waktu yang valid", + "error.validation.url": "Masukkan URL yang valid", + + "field.required": "Bidang ini wajib", + "field.files.empty": "Belum ada berkas yang dipilih", + "field.pages.empty": "Belum ada halaman yang dipilih", + "field.structure.delete.confirm": "Anda yakin menghapus baris ini?", + "field.structure.empty": "Belum ada entri", + "field.users.empty": "Belum ada pengguna yang dipilih", + + "file.delete.confirm": + "Anda yakin menghapus
{filename}?", + + "files": "Berkas", + "files.empty": "Belum ada berkas", + + "hour": "Jam", + "insert": "Sisipkan", + "install": "Pasang", + + "installation": "Pemasangan", + "installation.completed": "Panel sudah dipasang", + "installation.disabled": "Pemasang panel dimatikan di server publik secara bawaan. Mohon jalankan di server lokal atau ubah opsi panel.install untuk menjalankan di server saat ini.", + "installation.issues.accounts": + "Folder /site/accounts tidak ada atau tidak dapat ditulis", + "installation.issues.content": + "Folder /content tidak ada atau tidak dapat ditulis", + "installation.issues.curl": "Ekstensi CURL diperlukan", + "installation.issues.headline": "Panel tidak dapat dipasang", + "installation.issues.mbstring": + "Ekstensi MB String diperlukan", + "installation.issues.media": + "Folder /media tidak ada atau tidak dapat ditulis", + "installation.issues.php": "Pastikan Anda menggunakan PHP 7+", + "installation.issues.server": + "Kirby memerlukan Apache, Nginx, atau Caddy", + "installation.issues.sessions": "Folder /site/sessions tidak ada atau tidak dapat ditulis", + + "language": "Bahasa", + "language.code": "Kode", + "language.convert": "Atur sebagai bawaan", + "language.convert.confirm": + "

Anda yakin mengubah {name} menjadi bahasa bawaan? Ini tidak dapat dibatalkan.

Jika {name} memiliki konten yang tidak diterjemahkan, tidak akan ada pengganti yang valid dan dapat menyebabkan beberapa bagian dari situs Anda menjadi kosong.

", + "language.create": "Tambah bahasa baru", + "language.delete.confirm": + "Anda yakin menghapus bahasa {name} termasuk semua terjemahannya? Ini tidak dapat dibatalkan!", + "language.deleted": "Bahasa sudah dihapus", + "language.direction": "Arah baca", + "language.direction.ltr": "Kiri ke kanan", + "language.direction.rtl": "Kanan ke kiri", + "language.locale": "String \"PHP locale\"", + "language.locale.warning": "Anda menggunakan pengaturan lokal ubah suaian. Ubah di berkas bahasa di /site/languages", + "language.name": "Nama", + "language.updated": "Bahasa sudah diperbaharui", + + "languages": "Bahasa", + "languages.default": "Bahasa bawaan", + "languages.empty": "Belum ada bahasa", + "languages.secondary": "Bahasa sekunder", + "languages.secondary.empty": "Belum ada bahasa sekunder", + + "license": "Lisensi Kirby", + "license.buy": "Beli lisensi", + "license.register": "Daftar", + "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", + + "link": "Tautan", + "link.text": "Teks tautan", + + "loading": "Memuat", + + "lock.unsaved": "Perubahan belum tersimpan", + "lock.unsaved.empty": "Tidak ada lagi perubahan belum tersimpan", + "lock.isLocked": "Perubahan belum tersimpan oleh {email}", + "lock.file.isLocked": "Berkas sedang disunting oleh {email} dan tidak dapat diubah.", + "lock.page.isLocked": "Halaman sedang disunting oleh {email} dan tidak dapat diubah.", + "lock.unlock": "Buka kunci", + "lock.isUnlocked": "Perubahan Anda yang belum tersimpan telah terubah oleh pengguna lain. Anda dapat mengunduh perubahan Anda untuk menggabungkannya manual.", + + "login": "Masuk", + "login.remember": "Biarkan tetap masuk", + + "logout": "Keluar", + + "menu": "Menu", + "meridiem": "AM/PM", + "mime": "Tipe Media", + "minutes": "Menit", + + "month": "Bulan", + "months.april": "April", + "months.august": "Agustus", + "months.december": "Desember", + "months.february": "Februari", + "months.january": "Januari", + "months.july": "Juli", + "months.june": "Juni", + "months.march": "Maret", + "months.may": "Mei", + "months.november": "November", + "months.october": "Oktober", + "months.september": "September", + + "more": "Lebih lanjut", + "name": "Nama", + "next": "Selanjutnya", + "off": "mati", + "on": "hidup", + "open": "Buka", + "options": "Opsi", + + "orientation": "Orientasi", + "orientation.landscape": "Rebah", + "orientation.portrait": "Tegak", + "orientation.square": "Persegi", + + "page.changeSlug": "Ubah URL", + "page.changeSlug.fromTitle": "Buat dari judul", + "page.changeStatus": "Ubah status", + "page.changeStatus.position": "Pilih posisi", + "page.changeStatus.select": "Pilih status baru", + "page.changeTemplate": "Ubah templat", + "page.delete.confirm": + "Anda yakin menghapus {title}?", + "page.delete.confirm.subpages": + "Halaman ini memiliki sub-halaman.
Semua sub-halaman akan ikut dihapus.", + "page.delete.confirm.title": "Masukkan judul halaman untuk mengonfirmasi", + "page.draft.create": "Buat draf", + "page.duplicate.appendix": "Salin", + "page.duplicate.files": "Salin berkas", + "page.duplicate.pages": "Salin halaman", + "page.status": "Status", + "page.status.draft": "Draf", + "page.status.draft.description": + "Halaman hanya terlihat untuk penyunting", + "page.status.listed": "Publik", + "page.status.listed.description": "Halaman publik untuk siapapun", + "page.status.unlisted": "Tidak tercantum", + "page.status.unlisted.description": "Halaman hanya dapat diakses via URL", + + "pages": "Halaman", + "pages.empty": "Belum ada halaman", + "pages.status.draft": "Draf", + "pages.status.listed": "Dipublikasikan", + "pages.status.unlisted": "Tidak tercantum", + + "pagination.page": "Halaman", + + "password": "Sandi", + "pixel": "Piksel", + "prev": "Sebelumnya", + "remove": "Hapus", + "rename": "Ubah nama", + "replace": "Ganti", + "retry": "Coba lagi", + "revert": "Kembalikan", + + "role": "Peran", + "role.admin.description": "Admin memiliki semua izin", + "role.admin.title": "Admin", + "role.all": "Semua", + "role.empty": "Tidak ada pengguna dengan peran ini", + "role.description.placeholder": "Tidak ada deskripsi", + "role.nobody.description": "Ini adalah peran cadangan tanpa permisi apapun", + "role.nobody.title": "Tidak siapapun", + + "save": "Simpan", + "search": "Cari", + + "section.required": "Bagian ini wajib", + + "select": "Pilih", + "settings": "Pengaturan", + "size": "Ukuran", + "slug": "Akhiran URL", + "sort": "Urutkan", + "title": "Judul", + "template": "Templat", + "today": "Hari ini", + + "toolbar.button.code": "Kode", + "toolbar.button.bold": "Tebal", + "toolbar.button.email": "Surel", + "toolbar.button.headings": "Penajukan", + "toolbar.button.heading.1": "Penajukan 1", + "toolbar.button.heading.2": "Penajukan 2", + "toolbar.button.heading.3": "Penajukan 3", + "toolbar.button.italic": "Miring", + "toolbar.button.file": "Berkas", + "toolbar.button.file.select": "Pilih berkas", + "toolbar.button.file.upload": "Unggah berkas", + "toolbar.button.link": "Tautan", + "toolbar.button.ol": "Daftar berurut", + "toolbar.button.ul": "Daftar tidak berurut", + + "translation.author": "Tim Kirby", + "translation.direction": "ltr", + "translation.name": "Bahasa Indonesia", + "translation.locale": "id_ID", + + "upload": "Unggah", + "upload.error.cantMove": "Berkas unggahan tidak dapat dipindahkan", + "upload.error.cantWrite": "Gagal menyimpan berkas", + "upload.error.default": "Berkas tidak dapat diunggah", + "upload.error.extension": "Unggahan berkas diblokir dengan ekstensi", + "upload.error.formSize": "Berkas unggahan mencapai acuan MAX_FILE_SIZE yang diatur di formulir", + "upload.error.iniPostSize": "Berkas unggahan mencapai acuan post_max_size di php.ini", + "upload.error.iniSize": "Berkas unggahan mencapai acuan upload_max_filesize di php.ini", + "upload.error.noFile": "Tidak ada berkas diunggah", + "upload.error.noFiles": "Tidak ada berkas diunggah", + "upload.error.partial": "Berkas unggahan hanya berhasil diunggah sebagian", + "upload.error.tmpDir": "Folder sementara tidak ada", + "upload.errors": "Kesalahan", + "upload.progress": "Mengunggah…", + + "url": "Url", + "url.placeholder": "https://contoh.com", + + "user": "Pengguna", + "user.blueprint": + "Anda dapat mendefinisikan bagian tambahan dan bidang formulir untuk peran pengguna ini di /site/blueprints/users/{role}.yml", + "user.changeEmail": "Ubah surel", + "user.changeLanguage": "Ubah bahasa", + "user.changeName": "Ubah nama pengguna ini", + "user.changePassword": "Ubah sandi", + "user.changePassword.new": "Sandi baru", + "user.changePassword.new.confirm": "Konfirmasi sandi baru…", + "user.changeRole": "Ubah peran", + "user.changeRole.select": "Pilih peran baru", + "user.create": "Tambah pengguna baru", + "user.delete": "Hapus pengguna ini", + "user.delete.confirm": + "Anda yakin menghapus
{email}?", + + "users": "Pengguna", + + "version": "Versi", + + "view.account": "Akun Anda", + "view.installation": "Pemasangan", + "view.settings": "Pengaturan", + "view.site": "Situs", + "view.users": "Pengguna", + + "welcome": "Selamat datang", + "year": "Tahun" +} diff --git a/kirby/i18n/translations/it.json b/kirby/i18n/translations/it.json new file mode 100755 index 0000000..68101bc --- /dev/null +++ b/kirby/i18n/translations/it.json @@ -0,0 +1,481 @@ +{ + "add": "Aggiungi", + "avatar": "Immagine del profilo", + "back": "Indietro", + "cancel": "Annulla", + "change": "Cambia", + "close": "Chiudi", + "confirm": "OK", + "copy": "Copia", + "create": "Crea", + + "date": "Data", + "date.select": "Scegli una data", + + "day": "Giorno", + "days.fri": "Ve", + "days.mon": "Lu", + "days.sat": "Sa", + "days.sun": "Do", + "days.thu": "Gi", + "days.tue": "Ma", + "days.wed": "Me", + + "delete": "Elimina", + "dimensions": "Dimensioni", + "disabled": "Disabled", + "discard": "Abbandona", + "download": "Scarica", + "duplicate": "Duplica", + "edit": "Modifica", + + "dialog.files.empty": "Nessun file selezionabile", + "dialog.pages.empty": "Nessuna pagina selezionabile", + "dialog.users.empty": "Nessuno user selezionabile", + + "email": "Email", + "email.placeholder": "mail@esempio.com", + + "error.access.login": "Login Invalido", + "error.access.panel": "Non ti è permesso accedere al pannello", + "error.access.view": "You are not allowed to access this part of the panel", + + "error.avatar.create.fail": "Non è stato possibile caricare l'immagine del profilo", + "error.avatar.delete.fail": "Non è stato possibile eliminare l'immagine del profilo", + "error.avatar.dimensions.invalid": + "Per favore mantieni l'altezza e la larghezza dell'immagine del profilo inferiore ai 3000 pixel", + "error.avatar.mime.forbidden": + "L'immagine del profilo dev'essere un file JPEG o PNG", + + "error.blueprint.notFound": "Non è stato possibile caricare il blueprint \"{name}\"", + + "error.email.preset.notFound": "Non è stato possibile trovare il preset email \"{name}\"", + + "error.field.converter.invalid": "Convertitore \"{converter}\" non valido", + + "error.file.changeName.empty": "The name must not be empty", + "error.file.changeName.permission": + "Non ti è permesso modificare il nome di \"{filename}\"", + "error.file.duplicate": "Un file con il nome \"{filename}\" esiste già", + "error.file.extension.forbidden": + "L'estensione \"{extension}\" non è consentita", + "error.file.extension.missing": + "Il file \"{filename}\" non ha estensione", + "error.file.maxheight": "The height of the image must not exceed {height} pixels", + "error.file.maxsize": "The file is too large", + "error.file.maxwidth": "The width of the image must not exceed {width} pixels", + "error.file.mime.differs": + "Il file caricato dev'essere dello stesso MIME type \"{mime}\"", + "error.file.mime.forbidden": "Il MIME type \"{mime}\" non è consentito", + "error.file.mime.invalid": "Invalid mime type: {mime}", + "error.file.mime.missing": + "Il MIME type per \"{filename}\" non può essere rilevato", + "error.file.minheight": "The height of the image must be at least {height} pixels", + "error.file.minsize": "The file is too small", + "error.file.minwidth": "The width of the image must be at least {width} pixels", + "error.file.name.missing": "Il nome del file non può essere vuoto", + "error.file.notFound": "Il file non \u00e8 stato trovato", + "error.file.orientation": "The orientation of the image must be \"{orientation}\"", + "error.file.type.forbidden": "Non ti è permesso caricare file {type}", + "error.file.undefined": "Il file non \u00e8 stato trovato", + + "error.form.incomplete": "Correggi tutti gli errori nel form...", + "error.form.notSaved": "Non è stato possibile salvare il form", + + "error.language.code": "Inserisci un codice valido per la lingua", + "error.language.duplicate": "La lingua esiste già", + "error.language.name": "Inserisci un nome valido per la lingua", + + "error.license.format": "Inserisci un codice di licenza valido", + "error.license.email": "Inserisci un indirizzo email valido", + "error.license.verification": "Non è stato possibile verificare la licenza", + + "error.page.changeSlug.permission": + "Non ti è permesso cambiare l'URL di \"{slug}\"", + "error.page.changeStatus.incomplete": + "La pagina contiene errori e non può essere pubblicata", + "error.page.changeStatus.permission": + "Lo stato di questa pagina non può essere cambiato", + "error.page.changeStatus.toDraft.invalid": + "La pagina \"{slug}\" non può essere convertita in bozza", + "error.page.changeTemplate.invalid": + "Il template della pagina \"{slug}\" non può essere cambiato", + "error.page.changeTemplate.permission": + "Non ti è permesso modificare il template di \"{slug}\"", + "error.page.changeTitle.empty": "Il titolo non può essere vuoto", + "error.page.changeTitle.permission": + "Non ti è permesso modificare il titolo di \"{slug}\"", + "error.page.create.permission": "Non ti è permesso creare \"{slug}\"", + "error.page.delete": "La pagina \"{slug}\" non può essere eliminata", + "error.page.delete.confirm": "Inserisci il titolo della pagina per confermare", + "error.page.delete.hasChildren": + "La pagina ha sottopagine e non può essere eliminata", + "error.page.delete.permission": "Non ti è permesso eliminare \"{slug}\"", + "error.page.draft.duplicate": + "Una bozza di pagina con l'URL \"{slug}\" esiste già", + "error.page.duplicate": + "Una pagina con l'URL \"{slug}\" esiste già", + "error.page.duplicate.permission": "Non ti è permesso duplicare \"{slug}\"", + "error.page.notFound": "La pagina \"{slug}\" non è stata trovata", + "error.page.num.invalid": + "Inserisci un numero di ordinamento valido. I numeri non devono essere negativi", + "error.page.slug.invalid": "Inserisci un prefisso URL valido", + "error.page.sort.permission": "La pagina \"{slug}\" non può essere ordinata", + "error.page.status.invalid": "Imposta uno stato valido per la pagina", + "error.page.undefined": "La pagina non \u00e8 stata trovata", + "error.page.update.permission": "Non ti è permesso modificare \"{slug}\"", + + "error.section.files.max.plural": + "Non puoi aggiungere più di {max} file alla sezione \"{section}\"", + "error.section.files.max.singular": + "Non puoi aggiungere più di un file alla sezione \"{section}\"", + "error.section.files.min.plural": + "La sezione \"{section}\" richiede almeno {min} file", + "error.section.files.min.singular": + "La sezione \"{section}\" richiede almeno un file", + + "error.section.pages.max.plural": + "Non puoi aggiungere più di {max} pagine alla sezione \"{section}\"", + "error.section.pages.max.singular": + "Non puoi aggiungere più di una pagina alla sezione \"{section}\"", + "error.section.pages.min.plural": + "La sezione \"{section}\" richiede almeno {min} pagine", + "error.section.pages.min.singular": + "La sezione \"{section}\" richiede almeno una pagina", + + "error.section.notLoaded": "Non è stato possibile caricare la sezione \"{name}\"", + "error.section.type.invalid": "Il tipo di sezione \"{type}\" non è valido", + + "error.site.changeTitle.empty": "Il titolo non può essere vuoto", + "error.site.changeTitle.permission": + "Non ti è permesso modificare il titolo del sito", + "error.site.update.permission": "Non ti è permesso modificare i contenuti globali del sito", + + "error.template.default.notFound": "Il template \"default\" non esiste", + + "error.user.changeEmail.permission": + "Non ti è permesso modificare l'indirizzo email di \"{name}\"", + "error.user.changeLanguage.permission": + "Non ti è permesso modificare la lingua per l'utente \"{name}\"", + "error.user.changeName.permission": + "Non ti è permesso modificare il nome dell'utente \"{name}\"", + "error.user.changePassword.permission": + "Non ti è permesso modificare la password dell'utente \"{name}\"", + "error.user.changeRole.lastAdmin": + "Il ruolo dell'ultimo amministratore non può esser cambiato", + "error.user.changeRole.permission": + "Non ti è permesso modificare il ruolo dell'utente \"{name}\"", + "error.user.changeRole.toAdmin": + "You are not allowed to promote someone to the admin role", + "error.user.create.permission": "Non ti è permesso creare questo utente", + "error.user.delete": "L'utente non pu\u00f2 essere eliminato", + "error.user.delete.lastAdmin": "L'ultimo amministratore non può essere eliminato", + "error.user.delete.lastUser": "L'ultimo utente non può essere eliminato", + "error.user.delete.permission": + "Non ti \u00e8 permesso eliminare questo utente ", + "error.user.duplicate": + "Esiste già un utente con l'indirizzo email \"{email}\"", + "error.user.email.invalid": "Inserisci un indirizzo email valido", + "error.user.language.invalid": "Inserisci una lingua valida", + "error.user.notFound": "L'utente non \u00e8 stato trovato", + "error.user.password.invalid": + "Per favore inserisci una password valida. Le password devono essere lunghe almeno 8 caratteri", + "error.user.password.notSame": "Le password non corrispondono", + "error.user.password.undefined": "L'utente non ha una password", + "error.user.role.invalid": "Inserisci un ruolo valido", + "error.user.update.permission": + "Non ti è permesso aggiornare l'utente \"{name}\"", + + "error.validation.accepted": "Per favore conferma", + "error.validation.alpha": "Puoi inserire solo caratteri tra a-z", + "error.validation.alphanum": + "Puoi inserire solo caratteri tra a-z e numeri 0-9", + "error.validation.between": + "Inserisci un valore tra \"{min}\" e \"{max}\"", + "error.validation.boolean": "Per favore conferma o nega", + "error.validation.contains": + "Inserisci un valore che contiene \"{needle}\"", + "error.validation.date": "Inserisci una data valida", + "error.validation.date.after": "Inserisci una data dopo il {date}", + "error.validation.date.before": "Inserisci una data prima del {date}", + "error.validation.date.between": "Inserisci una data tra {min} e {max}", + "error.validation.denied": "Per favore nega", + "error.validation.different": "Il valore non dev'essere \"{other}\"", + "error.validation.email": "Inserisci un indirizzo email valido", + "error.validation.endswith": "Il valore non deve finire con \"{end}\"", + "error.validation.filename": "Inserisci un nome del file valido", + "error.validation.in": "Inserisci uno dei seguenti valori: ({in})", + "error.validation.integer": "Inserisci un numero intero", + "error.validation.ip": "Inserisci un indirizzo IP valido", + "error.validation.less": "Inserisci un valore inferiore a {max}", + "error.validation.match": "Il valore non corrisponde al pattern previsto", + "error.validation.max": "Inserisci un valore inferiore o uguale a {max}", + "error.validation.maxlength": + "Inserisci un testo più corto. (max. {max} caratteri)", + "error.validation.maxwords": "Non inserire più di {max} parola/e", + "error.validation.min": "Inserisci un valore superiore o uguale a {min}", + "error.validation.minlength": + "Inserisci un testo più lungo. (min. {min} caratteri)", + "error.validation.minwords": "Inserisci almeno {min} parola/e", + "error.validation.more": "Inserisci un valore superiore a {min}", + "error.validation.notcontains": + "Inserisci un valore che non contenga \"{needle}\"", + "error.validation.notin": + "Non inserire nessuno dei valori seguenti: ({notIn})", + "error.validation.option": "Seleziona un'opzione valida", + "error.validation.num": "Inserisci un numero valido", + "error.validation.required": "Inserisci qualcosa", + "error.validation.same": "Inserisci \"{other}\"", + "error.validation.size": "La dimensione del valore dev'essere \"{size}\"", + "error.validation.startswith": "Il valore deve iniziare con \"{start}\"", + "error.validation.time": "Inserisci un orario valido", + "error.validation.url": "Inserisci un URL valido", + + "field.required": "The field is required", + "field.files.empty": "Nessun file selezionato", + "field.pages.empty": "Nessuna pagina selezionata", + "field.structure.delete.confirm": "Vuoi veramente eliminare questo elemento?", + "field.structure.empty": "Non ci sono ancora elementi.", + "field.users.empty": "Nessun utente selezionato", + + "file.delete.confirm": + "Sei sicuro di voler eliminare questo file?", + + "files": "Files", + "files.empty": "Nessun file caricato", + + "hour": "Ora", + "insert": "Inserisci", + "install": "Installa", + + "installation": "Installazione", + "installation.completed": "Il pannello è stato installato", + "installation.disabled": "L'installazione del pannello è disabilitata di default sui server pubblici. Esegui l'installazione in locale oppure abilitala usando l'opzione panel.install.", + "installation.issues.accounts": + "/site/accounts non esiste o non dispone dei permessi di scrittura", + "installation.issues.content": + "La cartella /content non esiste o non dispone dei permessi di scrittura", + "installation.issues.curl": "È necessaria l'estensione CURL", + "installation.issues.headline": "Il pannello non può esser installato", + "installation.issues.mbstring": + "È necessaria l'estensione MB String", + "installation.issues.media": + "La cartella /media non esiste o non dispone dei permessi di scrittura", + "installation.issues.php": "Assicurati di utilizzare PHP 7.1+", + "installation.issues.server": + "Kirby necessita di Apache, Nginx o Caddy", + "installation.issues.sessions": "La cartella /site/sessionsnon esiste o non dispone dei permessi di scrittura", + + "language": "Lingua", + "language.code": "Codice", + "language.convert": "Imposta come predefinito", + "language.convert.confirm": + "

Sei sicuro di voler convertire {name} nella lingua predefinita? Questa operazione non può essere annullata.

Se {name} non contiene tutte le traduzioni, non ci sarà più una versione alternativa valida e parti del sito potrebbero rimanere vuote.

", + "language.create": "Aggiungi una nuova lingua", + "language.delete.confirm": + "Sei sicuro di voler eliminare la lingua {name} con tutte le traduzioni? Non sarà possibile annullare!", + "language.deleted": "La lingua è stata eliminata", + "language.direction": "Direzione di lettura", + "language.direction.ltr": "Sinistra a destra", + "language.direction.rtl": "Destra a sinistra", + "language.locale": "Stringa \"PHP locale\"", + "language.locale.warning": "Stai usando una impostazione personalizzata per il locale. Modificalo nel file della lingua situato in /site/languages", + "language.name": "Nome", + "language.updated": "La lingua è stata aggiornata", + + "languages": "Lingue", + "languages.default": "Lingua di default", + "languages.empty": "Non ci sono lingue impostate", + "languages.secondary": "Lingue secondarie", + "languages.secondary.empty": "Non ci sono lingue secondarie impostate", + + "license": "Licenza di Kirby", + "license.buy": "Acquista una licenza", + "license.register": "Registra", + "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", + + "link": "Link", + "link.text": "Testo del link", + + "loading": "Caricamento", + + "lock.unsaved": "Modifiche non salvate", + "lock.unsaved.empty": "There are no more unsaved changes", + "lock.isLocked": "Modifiche non salvate di {email}", + "lock.file.isLocked": "Il file viene attualmente modificato da {email} e non può essere cambiato.", + "lock.page.isLocked": "la pagina viene attualmente modificata da {email} e non può essere cambiata.", + "lock.unlock": "Sblocca", + "lock.isUnlocked": "Un altro utente ha sovrascritto le tue modifiche non salvate. Puoi scaricarle per recuperarle e quindi incorporarle manualmente. ", + + "login": "Accedi", + "login.remember": "Resta collegato", + + "logout": "Esci", + + "menu": "Menu", + "meridiem": "AM/PM", + "mime": "MIME Type", + "minutes": "Minuti", + + "month": "Mese", + "months.april": "Aprile", + "months.august": "Agosto", + "months.december": "Dicembre", + "months.february": "Febbraio", + "months.january": "Gennaio", + "months.july": "Luglio", + "months.june": "Giugno", + "months.march": "Marzo", + "months.may": "Maggio", + "months.november": "Novembre", + "months.october": "Ottobre", + "months.september": "Settembre", + + "more": "Di più", + "name": "Nome", + "next": "Prossimo", + "off": "off", + "on": "on", + "open": "Apri", + "options": "Opzioni", + + "orientation": "Orientamento", + "orientation.landscape": "Panorama", + "orientation.portrait": "Ritratto", + "orientation.square": "Quadrato", + + "page.changeSlug": "Modifica URL", + "page.changeSlug.fromTitle": "Crea in base al titolo", + "page.changeStatus": "Cambia stato", + "page.changeStatus.position": "Scegli una posizione", + "page.changeStatus.select": "Seleziona un nuovo stato", + "page.changeTemplate": "Cambia template", + "page.delete.confirm": + "Sei sicuro di voler eliminare questa pagina?", + "page.delete.confirm.subpages": + "Questa pagina ha sottopagine.
Anche tutte le sottopagine verranno eliminate.", + "page.delete.confirm.title": "Inserisci il titolo della pagina per confermare", + "page.draft.create": "Crea bozza", + "page.duplicate.appendix": "Copia", + "page.duplicate.files": "Copia file", + "page.duplicate.pages": "Copia pagine", + "page.status": "Stato", + "page.status.draft": "Bozza", + "page.status.draft.description": + "La pagina è in modalità bozza ed è visibile solo per editori registrati", + "page.status.listed": "Pubblico", + "page.status.listed.description": "La pagina è pubblicata per tutti", + "page.status.unlisted": "Non in elenco", + "page.status.unlisted.description": "La pagina è accessibile soltanto tramite URL", + + "pages": "Pagine", + "pages.empty": "Nessuna pagina", + "pages.status.draft": "Bozza", + "pages.status.listed": "Pubblicato", + "pages.status.unlisted": "Non in elenco", + + "pagination.page": "Pagina", + + "password": "Password", + "pixel": "Pixel", + "prev": "Precedente", + "remove": "Rimuovi", + "rename": "Rinomina", + "replace": "Sostituisci", + "retry": "Riprova", + "revert": "Abbandona", + + "role": "Ruolo", + "role.admin.description": "L'amministratore ha tutti i permessi", + "role.admin.title": "Amministratore", + "role.all": "Tutti", + "role.empty": "Non ci sono utenti con questo ruolo", + "role.description.placeholder": "Nessuna descrizione", + "role.nobody.description": "Questo è un ruolo \"fallback\" senza permessi", + "role.nobody.title": "Nessuno", + + "save": "Salva", + "search": "Cerca", + + "section.required": "The section is required", + + "select": "Seleziona", + "settings": "Impostazioni", + "size": "Dimensioni", + "slug": "URL", + "sort": "Ordina", + "title": "Titolo", + "template": "Template", + "today": "Oggi", + + "toolbar.button.code": "Codice", + "toolbar.button.bold": "Grassetto", + "toolbar.button.email": "Email", + "toolbar.button.headings": "Titoli", + "toolbar.button.heading.1": "Titolo 1", + "toolbar.button.heading.2": "Titolo 2", + "toolbar.button.heading.3": "Titolo 3", + "toolbar.button.italic": "Corsivo", + "toolbar.button.file": "File", + "toolbar.button.file.select": "Seleziona un file", + "toolbar.button.file.upload": "Carica un file", + "toolbar.button.link": "Link", + "toolbar.button.ol": "Elenco numerato", + "toolbar.button.ul": "Elenco puntato", + + "translation.author": "Kirby Team, Roman Steiner, Manu Moreale", + "translation.direction": "ltr", + "translation.name": "Italiano", + "translation.locale": "it_IT", + + "upload": "Carica", + "upload.error.cantMove": "The uploaded file could not be moved", + "upload.error.cantWrite": "Failed to write file to disk", + "upload.error.default": "The file could not be uploaded", + "upload.error.extension": "File upload stopped by extension", + "upload.error.formSize": "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the form", + "upload.error.iniPostSize": "The uploaded file exceeds the post_max_size directive in php.ini", + "upload.error.iniSize": "The uploaded file exceeds the upload_max_filesize directive in php.ini", + "upload.error.noFile": "No file was uploaded", + "upload.error.noFiles": "No files were uploaded", + "upload.error.partial": "The uploaded file was only partially uploaded", + "upload.error.tmpDir": "Missing a temporary folder", + "upload.errors": "Errore", + "upload.progress": "Caricamento...", + + "url": "URL", + "url.placeholder": "https://esempio.com", + + "user": "Utente", + "user.blueprint": + "Puoi definire sezioni e campi del form aggiuntivi per questo ruolo in /site/blueprints/users/{role}.yml", + "user.changeEmail": "Modifica email", + "user.changeLanguage": "Cambia lingua", + "user.changeName": "Rinomina questo utente", + "user.changePassword": "Cambia password", + "user.changePassword.new": "Nuova password", + "user.changePassword.new.confirm": "Conferma la nuova password...", + "user.changeRole": "Cambia ruolo", + "user.changeRole.select": "Seleziona un nuovo ruolo", + "user.create": "Aggiungi nuovo utente", + "user.delete": "Elimina questo utente", + "user.delete.confirm": + "Sei sicuro di voler eliminare questo utente?", + + "users": "Utenti", + + "version": "Versione di Kirby", + + "view.account": "Il tuo account", + "view.installation": "Installazione", + "view.settings": "Impostazioni", + "view.site": "Sito", + "view.users": "Utenti", + + "welcome": "Benvenuto", + "year": "Anno" +} diff --git a/kirby/i18n/translations/ko.json b/kirby/i18n/translations/ko.json new file mode 100755 index 0000000..d6e98c5 --- /dev/null +++ b/kirby/i18n/translations/ko.json @@ -0,0 +1,481 @@ +{ + "add": "\ucd94\uac00", + "avatar": "\ud504\ub85c\ud544 \uc774\ubbf8\uc9c0", + "back": "돌아가기", + "cancel": "\ucde8\uc18c", + "change": "\ubcc0\uacbd", + "close": "\ub2eb\uae30", + "confirm": "확인", + "copy": "복사", + "create": "등록", + + "date": "날짜", + "date.select": "날짜 선택", + + "day": "일", + "days.fri": "\uae08", + "days.mon": "\uc6d4", + "days.sat": "\ud1a0", + "days.sun": "\uc77c", + "days.thu": "\ubaa9", + "days.tue": "\ud654", + "days.wed": "\uc218", + + "delete": "\uc0ad\uc81c", + "dimensions": "크기", + "disabled": "비활성화", + "discard": "무시", + "download": "다운로드", + "duplicate": "복제", + "edit": "\ud3b8\uc9d1", + + "dialog.files.empty": "선택한 파일이 없습니다.", + "dialog.pages.empty": "선택한 페이지가 없습니다.", + "dialog.users.empty": "선택한 사용자가 없습니다.", + + "email": "\uc774\uba54\uc77c \uc8fc\uc18c", + "email.placeholder": "mail@example.com", + + "error.access.login": "로그인할 수 없습니다.", + "error.access.panel": "패널에 접근할 권한이 없습니다.", + "error.access.view": "패널에 접근할 권한이 없습니다.", + + "error.avatar.create.fail": "프로필 이미지를 업로드할 수 없습니다.", + "error.avatar.delete.fail": "\ud504\ub85c\ud544 \uc774\ubbf8\uc9c0\ub97c \uc0ad\uc81c\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "error.avatar.dimensions.invalid": + "프로필 이미지의 크기를 3,000픽셀 이하로 설정하세요.", + "error.avatar.mime.forbidden": + "\uc5c5\ub85c\ub4dc\ud560 \uc218 \uc5c6\ub294 MIME \ud615\uc2dd\uc785\ub2c8\ub2e4.", + + "error.blueprint.notFound": "블루프린트({name})를 불러올 수 없습니다.", + + "error.email.preset.notFound": "기본 이메일 주소({name})가 없습니다.", + + "error.field.converter.invalid": "컨버터({converter})가 올바르지 않습니다.", + + "error.file.changeName.empty": "이름을 입력하세요.", + "error.file.changeName.permission": + "파일명({filename})을 변경할 권한이 없습니다.", + "error.file.duplicate": "파일명이 같은 파일({filename})이 있습니다.", + "error.file.extension.forbidden": + "이 확장자({extension})는 업로드할 수 없습니다.", + "error.file.extension.missing": + "파일({filename})에 확장자가 없습니다.", + "error.file.maxheight": "이미지의 높이는 {height}픽셀을 초과할 수 없습니다.", + "error.file.maxsize": "파일이 너무 큽니다.", + "error.file.maxwidth": "이미지의 너비는 {width}픽셀을 초과할 수 없습니다.", + "error.file.mime.differs": + "기존 파일과 MIME 형식({mime})이 다릅니다.", + "error.file.mime.forbidden": "이 MIME 형식({mime})은 업로드할 수 없습니다.", + "error.file.mime.invalid": "MIME 형식({mime})이 올바르지 않습니다.", + "error.file.mime.missing": + "파일({filename})의 형식을 알 수 없습니다.", + "error.file.minheight": "이미지의 높이는 적어도 {height}픽셀 이상이어야 합니다.", + "error.file.minsize": "파일이 너무 작습니다.", + "error.file.minwidth": "이미지의 너비는 적어도 {width}픽셀 이상이어야 합니다.", + "error.file.name.missing": "파일명을 입력하세요.", + "error.file.notFound": "파일({filename})이 없습니다.", + "error.file.orientation": "이미지의 비율({orientation})을 확인하세요.", + "error.file.type.forbidden": "이 형식({type})의 파일을 업로드할 권한이 없습니다.", + "error.file.undefined": "\ud30c\uc77c\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.", + + "error.form.incomplete": "항목에 오류가 있습니다.", + "error.form.notSaved": "항목을 저장할 수 없습니다.", + + "error.language.code": "올바른 언어 코드를 입력하세요.", + "error.language.duplicate": "이미 등록된 언어입니다.", + "error.language.name": "올바른 언어명을 입력하세요.", + + "error.license.format": "올바른 라이선스 키를 입력하세요.", + "error.license.email": "올바른 이메일 주소를 입력하세요.", + "error.license.verification": "라이선스 키가 올바르지 않습니다.", + + "error.page.changeSlug.permission": + "고유 주소({slug})를 변경할 권한이 없습니다.", + "error.page.changeStatus.incomplete": + "페이지를 공개할 수 없습니다.", + "error.page.changeStatus.permission": + "페이지의 상태를 변경할 수 없습니다.", + "error.page.changeStatus.toDraft.invalid": + "페이지({slug})의 상태를 초안으로 변경할 수 없습니다.", + "error.page.changeTemplate.invalid": + "페이지({slug})의 템플릿을 변경할 수 없습니다.", + "error.page.changeTemplate.permission": + "페이지({slug})의 템플릿을 변경할 권한이 없습니다.", + "error.page.changeTitle.empty": "제목을 입력하세요.", + "error.page.changeTitle.permission": + "페이지({slug})의 제목을 삭제할 권한이 없습니다.", + "error.page.create.permission": "페이지({slug})를 등록할 권한이 없습니다.", + "error.page.delete": "페이지({slug})를 삭제할 수 없습니다.", + "error.page.delete.confirm": "페이지 제목을 입력하세요.", + "error.page.delete.hasChildren": + "하위 페이지가 있는 페이지는 삭제할 수 없습니다.", + "error.page.delete.permission": "페이지({slug})를 삭제할 권한이 없습니다.", + "error.page.draft.duplicate": + "고유 주소({slug})가 같은 초안이 있습니다.", + "error.page.duplicate": + "고유 주소({slug})가 같은 페이지가 있습니다.", + "error.page.duplicate.permission": "페이지({slug})를 복제할 권한이 없습니다.", + "error.page.notFound": "페이지({slug})가 없습니다.", + "error.page.num.invalid": + "올바른 정수를 입력하세요.", + "error.page.slug.invalid": "올바른 접두사를 입력하세요.", + "error.page.sort.permission": "페이지({slug})를 정렬할 수 없습니다.", + "error.page.status.invalid": "올바른 상태를 설정하세요.", + "error.page.undefined": "\ud398\uc774\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.", + "error.page.update.permission": "페이지({slug})를 변경할 권한이 없습니다.", + + "error.section.files.max.plural": + "이 섹션({section})에는 파일을 {max}개 이상 추가할 수 없습니다.", + "error.section.files.max.singular": + "이 섹션({section})에는 파일을 하나 이상 추가할 수 없습니다.", + "error.section.files.min.plural": + "이 섹션({section})에는 적어도 {min}개 이상의 파일이 필요합니다.", + "error.section.files.min.singular": + "이 섹션({section})에는 적어도 하나 이상의 파일이 필요합니다.", + + "error.section.pages.max.plural": + "이 섹션({section})에는 페이지를 {max}개 이상 추가할 수 없습니다.", + "error.section.pages.max.singular": + "이 섹션({section})에는 페이지를 하나 이상 추가할 수 없습니다.", + "error.section.pages.min.plural": + "이 섹션({section})에 적어도 {min}개 이상의 페이지가 필요합니다.", + "error.section.pages.min.singular": + "이 섹션({section})에 적어도 하나 이상의 페이지가 필요합니다.", + + "error.section.notLoaded": "섹션({name})을 불러올 수 없습니다.", + "error.section.type.invalid": "섹션의 형식({type})이 올바르지 않습니다.", + + "error.site.changeTitle.empty": "제목을 입력하세요.", + "error.site.changeTitle.permission": + "사이트명을 변경할 권한이 없습니다.", + "error.site.update.permission": "사이트의 정보를 변경할 권한이 없습니다.", + + "error.template.default.notFound": "기본 템플릿이 없습니다.", + + "error.user.changeEmail.permission": + "사용자({name})의 이메일 주소를 변경할 권한이 없습니다.", + "error.user.changeLanguage.permission": + "사용자({name})의 언어를 변경할 권한이 없습니다.", + "error.user.changeName.permission": + "사용자명({name})을 변경할 권한이 없습니다.", + "error.user.changePassword.permission": + "사용자({name})의 암호를 변경할 권한이 없습니다.", + "error.user.changeRole.lastAdmin": + "최종 관리자의 역할은 변경할 수 없습니다.", + "error.user.changeRole.permission": + "사용자({name})의 역할을 변경할 권한이 없습니다.", + "error.user.changeRole.toAdmin": + "다른 사용자를 관리자로 지정할 권한이 없습니다.", + "error.user.create.permission": "사용자를 등록할 권한이 없습니다.", + "error.user.delete": "사용자({name})를 삭제할 수 없습니다.", + "error.user.delete.lastAdmin": "\ucd5c\uc885 \uad00\ub9ac\uc790\ub294 \uc0ad\uc81c\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "error.user.delete.lastUser": "최종 사용자는 삭제할 수 없습니다.", + "error.user.delete.permission": + "사용자({name})를 삭제할 권한이 없습니다.", + "error.user.duplicate": + "이메일 주소({email})가 같은 사용자가 있습니다.", + "error.user.email.invalid": "올바른 이메일 주소를 입력하세요.", + "error.user.language.invalid": "올바른 언어를 입력하세요.", + "error.user.notFound": "사용자({name})가 없습니다.", + "error.user.password.invalid": + "올바른 암호를 입력하세요.", + "error.user.password.notSame": "\uc554\ud638\ub97c \ud655\uc778\ud558\uc138\uc694.", + "error.user.password.undefined": "암호가 설정되지 않았습니다.", + "error.user.role.invalid": "올바른 역할을 입력하세요.", + "error.user.update.permission": + "사용자({name})의 정보를 변경할 권한이 없습니다.", + + "error.validation.accepted": "확인하세요.", + "error.validation.alpha": "알파벳만 입력할 수 있습니다.", + "error.validation.alphanum": + "알파벳 또는 숫자만 입력할 수 있습니다.", + "error.validation.between": + "{min}과 {max} 사이의 값을 입력하세요.", + "error.validation.boolean": "확인하거나 취소하세요.", + "error.validation.contains": + "{needle}에 포함된 값을 입력하세요.", + "error.validation.date": "올바른 날짜를 입력하세요.", + "error.validation.date.after": "{date} 이후 날짜를 입력하세요.", + "error.validation.date.before": "{date} 이전 날짜를 입력하세요.", + "error.validation.date.between": "{min}, {max} 사이의 날짜를 입력하세요.", + "error.validation.denied": "취소하세요.", + "error.validation.different": "{other}에 포함된 값은 입력할 수 없습니다.", + "error.validation.email": "올바른 이메일 주소를 입력하세요.", + "error.validation.endswith": "값은 {end}(으)로 끝나야 합니다.", + "error.validation.filename": "올바른 파일명을 입력하세요.", + "error.validation.in": "{in}에 포함된 값을 입력하세요.", + "error.validation.integer": "올바른 정수를 입력하세요.", + "error.validation.ip": "올바른 IP 주소를 입력하세요.", + "error.validation.less": "{max}보다 작은 값을 입력하세요.", + "error.validation.match": "입력한 값이 예상 패턴과 일치하지 않습니다.", + "error.validation.max": "{max} 이하의 값을 입력하세요.", + "error.validation.maxlength": + "{max} 이하의 값을 입력하세요.", + "error.validation.maxwords": "{max}자 이하의 값을 입력하세요.", + "error.validation.min": "{min} 이상의 값을 입력하세요.", + "error.validation.minlength": + "{min} 이상의 값을 입력하세요.", + "error.validation.minwords": "{min}자 이상의 값을 입력하세요.", + "error.validation.more": "{min} 이상의 값을 입력하세요.", + "error.validation.notcontains": + "{needle}에 포함된 값은 입력할 수 없습니다.", + "error.validation.notin": + "{notIn}에 포함된 값은 입력할 수 없습니다.", + "error.validation.option": "올바른 옵션을 선택하세요.", + "error.validation.num": "올바른 숫자를 입력하세요.", + "error.validation.required": "아무거나 입력하세요.", + "error.validation.same": "{other}를(을) 입력하세요.", + "error.validation.size": "값은 {size}과(와) 같아야 합니다.", + "error.validation.startswith": "값은 {start}(으)로 시작해야 합니다.", + "error.validation.time": "올바른 시간을 입력하세요.", + "error.validation.url": "올바른 URL을 입력하세요.", + + "field.required": "필드가 필요합니다.", + "field.files.empty": "선택된 파일이 없습니다.", + "field.pages.empty": "선택된 페이지가 없습니다.", + "field.structure.delete.confirm": "이 행을 삭제할까요?", + "field.structure.empty": "항목이 없습니다.", + "field.users.empty": "선택된 사용자가 없습니다.", + + "file.delete.confirm": + "파일({filename})을 삭제할까요?", + + "files": "파일", + "files.empty": "파일이 없습니다.", + + "hour": "시간", + "insert": "\uc0bd\uc785", + "install": "설치", + + "installation": "설치", + "installation.completed": "패널을 설치했습니다.", + "installation.disabled": "패널 설치 관리자는 로컬 서버에서 실행하거나 panel.install 옵션을 설정하세요.", + "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.php": "PHP 버전이 7 이상인지 확인하세요.", + "installation.issues.server": + "Apache, Nginx, 또는 Caddy가 필요합니다.", + "installation.issues.sessions": "폴더(/site/sessions)에 쓰기 권한이 없습니다.", + + "language": "\uc5b8\uc5b4", + "language.code": "언어 코드", + "language.convert": "기본 언어로 설정", + "language.convert.confirm": + "이 언어({name})를 기본 언어로 설정할까요? 설정한 뒤에는 복원할 수 없으며, 번역되지 않은 항목은 올바르게 표시되지 않을 수 있습니다.", + "language.create": "새 언어 추가", + "language.delete.confirm": + "언어({name})를 삭제할까요? 삭제한 뒤에는 복원할 수 없습니다.", + "language.deleted": "언어를 삭제했습니다.", + "language.direction": "읽기 방향", + "language.direction.ltr": "왼쪽에서 오른쪽", + "language.direction.rtl": "오른쪽에서 왼쪽", + "language.locale": "PHP 로캘 문자열", + "language.locale.warning": "커스텀 언어 설정를 사용 중입니다. /site/languages 폴더의 언어 파일을 수정하세요.", + "language.name": "이름", + "language.updated": "언어를 변경했습니다.", + + "languages": "언어", + "languages.default": "기본 언어", + "languages.empty": "언어가 없습니다.", + "languages.secondary": "보조 언어", + "languages.secondary.empty": "보조 언어가 없습니다.", + + "license": "라이선스", + "license.buy": "라이선스 구매", + "license.register": "등록", + "license.register.help": + "이메일 주소로 라이선스 코드를 전송했습니다. Kirby를 등록하려면 라이선스 코드와 이메일 주소를 입력하세요.", + "license.register.label": "라이선스 코드를 입력하세요.", + "license.register.success": "Kirby를 구입해주셔서 감사합니다.", + "license.unregistered": "Kirby가 등록되지 않았습니다.", + + "link": "\uc77c\ubc18 \ub9c1\ud06c", + "link.text": "\ubb38\uc790", + + "loading": "로딩 중", + + "lock.unsaved": "수정 사항이 저장되지 않았습니다.", + "lock.unsaved.empty": "저장되지 않은 페이지가 없습니다.", + "lock.isLocked": "다른 사용자({email})가 수정한 사항이 저장되지 않았습니다.", + "lock.file.isLocked": "다른 사용자({email})가 수정 중인 파일입니다.", + "lock.page.isLocked": "다른 사용자({email}가 수정 중인 페이지입니다.", + "lock.unlock": "잠금", + "lock.isUnlocked": "다른 사용자가 이미 내용을 수정했으므로 현재 내용이 올바르게 저장되지 않았습니다. 저장되지 않은 내용을 내려받아 수동으로 대치할 수 있습니다.", + + "login": "\ub85c\uadf8\uc778", + "login.remember": "로그인 유지", + + "logout": "\ub85c\uadf8\uc544\uc6c3", + + "menu": "메뉴", + "meridiem": "오전/오후", + "mime": "형식", + "minutes": "분", + + "month": "월", + "months.april": "4\uc6d4", + "months.august": "8\uc6d4", + "months.december": "12\uc6d4", + "months.february": "2\uc6d4", + "months.january": "1\uc6d4", + "months.july": "7\uc6d4", + "months.june": "6\uc6d4", + "months.march": "3\uc6d4", + "months.may": "5\uc6d4", + "months.november": "11\uc6d4", + "months.october": "10\uc6d4", + "months.september": "9\uc6d4", + + "more": "더 보기", + "name": "이름", + "next": "다음", + "off": "끔", + "on": "켬", + "open": "열기", + "options": "옵션", + + "orientation": "비율", + "orientation.landscape": "가로로 긴 사각형", + "orientation.portrait": "세로로 긴 사각형", + "orientation.square": "정사각형", + + "page.changeSlug": "고유 주소 변경", + "page.changeSlug.fromTitle": "\uc81c\ubaa9(\uc601\ubb38)\uc5d0\uc11c \uac00\uc838\uc624\uae30", + "page.changeStatus": "상태 변경", + "page.changeStatus.position": "위치를 선택하세요.", + "page.changeStatus.select": "새 상태 선택", + "page.changeTemplate": "템플릿 변경", + "page.delete.confirm": + "페이지({title})를 삭제할까요?", + "page.delete.confirm.subpages": + "페이지에 하위 페이지가 있습니다. 모든 하위 페이지가 삭제됩니다.", + "page.delete.confirm.title": "페이지 제목을 입력하세요.", + "page.draft.create": "초안 작성", + "page.duplicate.appendix": "복사", + "page.duplicate.files": "파일 복사", + "page.duplicate.pages": "페이지 복사", + "page.status": "상태", + "page.status.draft": "초안", + "page.status.draft.description": + "로그인한 사용자만 읽을 수 있습니다.", + "page.status.listed": "공개", + "page.status.listed.description": "누구나 읽을 수 있습니다.", + "page.status.unlisted": "비공개", + "page.status.unlisted.description": "URL을 통해서만 접근할 수 있습니다.", + + "pages": "하위 페이지", + "pages.empty": "페이지가 없습니다.", + "pages.status.draft": "초안", + "pages.status.listed": "발행", + "pages.status.unlisted": "비공개", + + "pagination.page": "페이지", + + "password": "\uc554\ud638", + "pixel": "픽셀", + "prev": "이전", + "remove": "삭제", + "rename": "제목 변경", + "replace": "\uad50\uccb4", + "retry": "\ub2e4\uc2dc \uc2dc\ub3c4", + "revert": "복원", + + "role": "역할", + "role.admin.description": "관리자는 모든 권한이 있습니다.", + "role.admin.title": "관리자", + "role.all": "전체 보기", + "role.empty": "이 역할에 해당하는 사용자가 없습니다.", + "role.description.placeholder": "설명이 없습니다.", + "role.nobody.description": "대체 사용자는 아무 권한이 없습니다.", + "role.nobody.title": "사용자가 없습니다.", + + "save": "\uc800\uc7a5", + "search": "검색", + + "section.required": "섹션이 필요합니다.", + + "select": "선택", + "settings": "설정", + "size": "크기", + "slug": "고유 주소", + "sort": "정렬", + "title": "제목", + "template": "\ud15c\ud50c\ub9bf", + "today": "오늘", + + "toolbar.button.code": "코드", + "toolbar.button.bold": "강조 1", + "toolbar.button.email": "이메일 주소", + "toolbar.button.headings": "제목", + "toolbar.button.heading.1": "제목 1", + "toolbar.button.heading.2": "제목 2", + "toolbar.button.heading.3": "제목 3", + "toolbar.button.italic": "강조 2", + "toolbar.button.file": "파일", + "toolbar.button.file.select": "파일 선택", + "toolbar.button.file.upload": "파일 업로드", + "toolbar.button.link": "링크", + "toolbar.button.ol": "숫자 목록", + "toolbar.button.ul": "기호 목록", + + "translation.author": "Kirby 팀", + "translation.direction": "LTR", + "translation.name": "한국어", + "translation.locale": "ko_KR", + + "upload": "업로드", + "upload.error.cantMove": "업로드한 파일을 이동할 수 없습니다.", + "upload.error.cantWrite": "디스크를 읽을 수 없습니다.", + "upload.error.default": "파일을 업로드할 수 없습니다.", + "upload.error.extension": "파일 확장자를 다시 한번 확인하세요.", + "upload.error.formSize": "허용된 크기를 초과해 파일을 업로드할 수 없습니다.", + "upload.error.iniPostSize": "허용된 크기를 초과해 파일을 업로드할 수 없습니다.", + "upload.error.iniSize": "허용된 크기를 초과해 파일을 업로드할 수 없습니다.", + "upload.error.noFile": "업로드한 파일이 없습니다.", + "upload.error.noFiles": "업로드한 파일이 없습니다.", + "upload.error.partial": "일부 파일만 업로드했습니다.", + "upload.error.tmpDir": "임시 폴더가 없습니다.", + "upload.errors": "오류", + "upload.progress": "업로드 중…", + + "url": "URL", + "url.placeholder": "https://example.com", + + "user": "사용자", + "user.blueprint": + "/site/blueprints/users/{role}.yml 파일에 섹션 및 폼 필드를 추가할 수 있습니다.", + "user.changeEmail": "이메일 주소 변경", + "user.changeLanguage": "언어 변경", + "user.changeName": "사용자명 변경", + "user.changePassword": "암호 변경", + "user.changePassword.new": "새 암호", + "user.changePassword.new.confirm": "새 암호 확인", + "user.changeRole": "역할 변경", + "user.changeRole.select": "새 역할 선택", + "user.create": "사용자 추가", + "user.delete": "사용자 삭제", + "user.delete.confirm": + "사용자({email})를 삭제할까요?", + + "users": "사용자", + + "version": "버전", + + "view.account": "계정", + "view.installation": "\uc124\uce58", + "view.settings": "설정", + "view.site": "사이트", + "view.users": "\uc0ac\uc6a9\uc790", + + "welcome": "환영합니다.", + "year": "년" +} diff --git a/kirby/i18n/translations/lt.json b/kirby/i18n/translations/lt.json new file mode 100755 index 0000000..bf14bed --- /dev/null +++ b/kirby/i18n/translations/lt.json @@ -0,0 +1,481 @@ +{ + "add": "Pridėti", + "avatar": "Profilio nuotrauka", + "back": "Atgal", + "cancel": "Atšaukti", + "change": "Keisti", + "close": "Uždaryti", + "confirm": "Ok", + "copy": "Kopijuoti", + "create": "Sukurti", + + "date": "Data", + "date.select": "Pasirinkite datą", + + "day": "Diena", + "days.fri": "Pen", + "days.mon": "Pir", + "days.sat": "Šeš", + "days.sun": "Sek", + "days.thu": "Ket", + "days.tue": "Ant", + "days.wed": "Tre", + + "delete": "Pašalinti", + "dimensions": "Išmatavimai", + "disabled": "Išjungta", + "discard": "Atšaukti", + "download": "Parsisiųsti", + "duplicate": "Kopijuoti", + "edit": "Redaguoti", + + "dialog.files.empty": "Nėra failų pasirinkimui", + "dialog.pages.empty": "Nėra puslapių pasirinkimui", + "dialog.users.empty": "Nėra vartotojų pasirinkimui", + + "email": "El. paštas", + "email.placeholder": "mail@example.com", + + "error.access.login": "Neteisingas prisijungimo vardas", + "error.access.panel": "Neturite teisės prisijungti prie valdymo pulto", + "error.access.view": "Neturite teisės peržiūrėti šios valdymo pulto dalies", + + "error.avatar.create.fail": "Nepavyko įkelti profilio nuotraukos", + "error.avatar.delete.fail": "Nepavyko pašalinti profilio nuotraukos", + "error.avatar.dimensions.invalid": + "Profilio nuotraukos plotis ar aukštis turėtų būti iki 3000 pikselių", + "error.avatar.mime.forbidden": + "Profilio nuotrauka turi būti JPEG arba PNG", + + "error.blueprint.notFound": "Blueprint \"{name}\" negali būti užkrautas", + + "error.email.preset.notFound": "El. pašto paruoštukas \"{name}\" nerastas", + + "error.field.converter.invalid": "Neteisingas konverteris \"{converter}\"", + + "error.file.changeName.empty": "Pavadinimas negali būti tuščias", + "error.file.changeName.permission": + "Neturite teisės pakeisti failo pavadinimo \"{filename}\"", + "error.file.duplicate": "Failas su pavadinimu \"{filename}\" jau yra", + "error.file.extension.forbidden": + "Failo tipas (plėtinys) \"{extension}\" neleidžiamas", + "error.file.extension.missing": + "Failui \"{filename}\" trūksta tipo (plėtinio)", + "error.file.maxheight": "Failo aukštis neturi viršyti {height} px", + "error.file.maxsize": "Failas per didelis", + "error.file.maxwidth": "Failo plotis neturi viršyti {width} px", + "error.file.mime.differs": + "Įkėliamas failas turi būti tokio pat mime tipo \"{mime}\"", + "error.file.mime.forbidden": "Media tipas \"{mime}\" neleidžiamas", + "error.file.mime.invalid": "Neteisingas mime tipas: {mime}", + "error.file.mime.missing": + "Failui \"{filename}\" nepavyko atpažinti media (mime) tipo", + "error.file.minheight": "Failo aukštis turi būti bent {height} px", + "error.file.minsize": "Failas per mažas", + "error.file.minwidth": "Failo plotis turi būti bent {width} px", + "error.file.name.missing": "Failo pavadinimas negali būti tuščias", + "error.file.notFound": "Failas \"{filename}\" nerastas", + "error.file.orientation": "Failo orientacija turi būti \"{orientation}\"", + "error.file.type.forbidden": "Jūs neturite teisės įkelti {type} tipo failų", + "error.file.undefined": "Failas nerastas", + + "error.form.incomplete": "🙏 Prašome ištaisyti visas formos klaidas…", + "error.form.notSaved": "Formos nepavyko išsaugoti", + + "error.language.code": "Prašome įrašyti teisingą kalbos kodą", + "error.language.duplicate": "Tokia kalba jau yra", + "error.language.name": "Prašome įrašyti teisingą kalbos pavadinimą", + + "error.license.format": "Prašome įrašyti teisingą licenzijos kodą", + "error.license.email": "Prašome įrašyti teisingą el. pašto adresą", + "error.license.verification": "Nepavyko patikrinti licenzijos", + + "error.page.changeSlug.permission": + "Neturite teisės pakeisti \"{slug}\" URL", + "error.page.changeStatus.incomplete": + "Puslapis turi klaidų ir negali būti paskelbtas", + "error.page.changeStatus.permission": + "Šiam puslapiui negalima pakeisti statuso", + "error.page.changeStatus.toDraft.invalid": + "Puslapio \"{slug}\" negalima paversti juodraščiu", + "error.page.changeTemplate.invalid": + "Šablono puslapiui \"{slug}\" negalima keisti", + "error.page.changeTemplate.permission": + "Neturite leidimo keisti šabloną puslapiui \"{slug}\"", + "error.page.changeTitle.empty": "Pavadinimas negali būti tuščias", + "error.page.changeTitle.permission": + "Neturite leidimo keisti pavadinimo puslapiui \"{slug}\"", + "error.page.create.permission": "Neturite leidimo sukurti \"{slug}\"", + "error.page.delete": "Puslapio \"{slug}\" negalima pašalinti", + "error.page.delete.confirm": "Įrašykite puslapio pavadinimą, tam kad patvirtintumėte", + "error.page.delete.hasChildren": + "Puslapis turi vidinių puslapių, dėl to negalima jo pašalinti", + "error.page.delete.permission": "Neturite leidimo šalinti \"{slug}\"", + "error.page.draft.duplicate": + "Puslapio juodraštis su URL pabaiga \"{slug}\" jau yra", + "error.page.duplicate": + "Puslapis su URL pabaiga \"{slug}\" jau yra", + "error.page.duplicate.permission": "Neturite leidimo dubliuoti \"{slug}\"", + "error.page.notFound": "Puslapis \"{slug}\" nerastas", + "error.page.num.invalid": + "Įrašykite teisingą eiliškumo numerį. Numeris negali būti neigiamas.", + "error.page.slug.invalid": "Įrašykite teisingą URL prefiksą", + "error.page.sort.permission": "Puslapiui \"{slug}\" negalima pakeisti eiliškumo", + "error.page.status.invalid": "Nustatykite teisingą puslapio statusą", + "error.page.undefined": "Puslapis nerastas", + "error.page.update.permission": "Neturite leidimo atnaujinti \"{slug}\"", + + "error.section.files.max.plural": + "Į sekciją \"{section}\" negalima pridėti daugiau nei {max} failų", + "error.section.files.max.singular": + "Į sekciją \"{section}\" negalima pridėti daugiau nei vieną failą", + "error.section.files.min.plural": + "Sekcija \"{section}\" reikalauja bent {min} failų", + "error.section.files.min.singular": + "Sekcija \"{section}\" reikalauja bent vieno failo", + + "error.section.pages.max.plural": + "Į sekciją \"{section}\" negalima pridėti daugiau nei {max} puslapių", + "error.section.pages.max.singular": + "Į sekciją \"{section}\" negalima pridėti daugiau nei vieną puslapį", + "error.section.pages.min.plural": + "Sekcija \"{section}\" reikalauja bent {min} puslapių", + "error.section.pages.min.singular": + "Sekcija \"{section}\" reikalauja bent vieno puslapio", + + "error.section.notLoaded": "Sekcija \"{name}\" negali būti užkrauta", + "error.section.type.invalid": "Sekcijos tipas \"{type}\" yra neteisingas", + + "error.site.changeTitle.empty": "Pavadinimas negali būti tuščias", + "error.site.changeTitle.permission": + "Neturite leidimo keisti svetainės pavadinimo", + "error.site.update.permission": "Neturite leidimo atnaujinti svetainės", + + "error.template.default.notFound": "Nėra šablono pagal nutylėjimą", + + "error.user.changeEmail.permission": + "Neturite leidimo keisti vartotojo \"{name}\" el. paštą", + "error.user.changeLanguage.permission": + "Neturite leidimo keisti vartotojo \"{name}\" kalbą", + "error.user.changeName.permission": + "Neturite leidimo keisti vartotojo \"{name}\" vardą", + "error.user.changePassword.permission": + "Neturite leidimo keisti vartotojo \"{name}\" slaptažodį", + "error.user.changeRole.lastAdmin": + "Vienintelio administratoriaus rolės negalima pakeisti", + "error.user.changeRole.permission": + "Neturite leidimo pakeisti vartotojo \"{name}\" rolės", + "error.user.changeRole.toAdmin": + "Jūs neturite teisių suteikti administratoriaus rolę", + "error.user.create.permission": "Neturite leidimo sukurti šį vartotoją", + "error.user.delete": "Vartotojo \"{name}\" negalima pašalinti", + "error.user.delete.lastAdmin": "Vienintelio administratoriaus negalima pašalinti", + "error.user.delete.lastUser": "Vienintelio vartotojo negalima pašalinti", + "error.user.delete.permission": + "Neturite leidimo pašalinti vartotoją \"{name}\"", + "error.user.duplicate": + "Vartotojas su el. paštu \"{email}\" jau yra", + "error.user.email.invalid": "Įrašykite teisingą el. pašto adresą", + "error.user.language.invalid": "Įrašykite teisingą kalbą", + "error.user.notFound": "Vartotojas \"{name}\" nerastas", + "error.user.password.invalid": + "Prašome įrašyti galiojantį slaptažodį. Slaptažodį turi sudaryti bent 8 simboliai.", + "error.user.password.notSame": "Slaptažodžiai nesutampa", + "error.user.password.undefined": "Vartotojas neturi slaptažodžio", + "error.user.role.invalid": "Įrašykite teisingą rolę", + "error.user.update.permission": + "Neturite teisės keisti vartotojo \"{name}\"", + + "error.validation.accepted": "Prašome patvirtinti", + "error.validation.alpha": "Prašome įrašyti tik raides a-z", + "error.validation.alphanum": + "Prašome įrašyti tik raides a-z arba skaičius 0-9", + "error.validation.between": + "Prašome įrašyti reikšmę tarp \"{min}\" ir \"{max}\"", + "error.validation.boolean": "Patvirtinkite arba atšaukite", + "error.validation.contains": + "Prašome įrašyti reikšmę, kuri turėtų \"{needle}\"", + "error.validation.date": "Prašome įrašyti korektišką datą", + "error.validation.date.after": "Įrašykite datą nuo {date}", + "error.validation.date.before": "Įrašykite datą iki {date}", + "error.validation.date.between": "Įrašykite datą tarp {min} ir {max}", + "error.validation.denied": "Prašome neleisti", + "error.validation.different": "Reikšmė neturi būti \"{other}\"", + "error.validation.email": "Prašome įrašyti korektišką el. paštą", + "error.validation.endswith": "Reikšmė turi baigtis su \"{end}\"", + "error.validation.filename": "Prašome įrašyti teisingą failo pavadinimą", + "error.validation.in": "Prašome įrašyti vieną iš šių: ({in})", + "error.validation.integer": "Prašome įrašyti teisingą sveiką skaičių", + "error.validation.ip": "Prašome įrašyti teisingą IP adresą", + "error.validation.less": "Prašome įrašyti mažiau nei {max}", + "error.validation.match": "Reikšmė nesutampa su laukiamu šablonu", + "error.validation.max": "Prašome įrašyti reikšmę lygią arba didesnę, nei {max}", + "error.validation.maxlength": + "Prašome įrašyti trumpesnę reikšmę. (max. {max} characters)", + "error.validation.maxwords": "Please enter no more than {max} word(s)", + "error.validation.min": "Please enter a value equal to or greater than {min}", + "error.validation.minlength": + "Prašome įrašyti ilgesnę reikšmę. (min. {min} characters)", + "error.validation.minwords": "Prašome įrašyti bent {min} žodžius", + "error.validation.more": "Prašome įrašyti daugiau nei {min}", + "error.validation.notcontains": + "Prašome įrašyti reikšmę, kuri neturi \"{needle}\"", + "error.validation.notin": + "Prašome neįrašyti vieną iš šių: ({notIn})", + "error.validation.option": "Prašome pasirinkti korektišką opciją", + "error.validation.num": "Prašome įrašyti teisingą numerį", + "error.validation.required": "Prašome įrašyti ką nors", + "error.validation.same": "Prašome įrašyti \"{other}\"", + "error.validation.size": "Reikšmės dydis turi būti \"{size}\"", + "error.validation.startswith": "Reikšmė turi prasidėti su \"{start}\"", + "error.validation.time": "Prašome įrašyti korektišką laiką", + "error.validation.url": "Prašome įrašyti teisingą URL", + + "field.required": "Laukas privalomas", + "field.files.empty": "Pasirinkti", + "field.pages.empty": "Dar nėra puslapių", + "field.structure.delete.confirm": "Ar tikrai norite pašalinti šią eilutę?", + "field.structure.empty": "Dar nėra įrašų", + "field.users.empty": "Dar nėra vartotojų", + + "file.delete.confirm": + "Ar tikrai norite pašalinti
{filename}?", + + "files": "Failai", + "files.empty": "Įkelti", + + "hour": "Valanda", + "insert": "Įterpti", + "install": "Įdiegti", + + "installation": "Įdiegimas", + "installation.completed": "Valdymo pultas įdiegtas", + "installation.disabled": "Pagal nutylėjimą valdymo pulto įdiegimas viešuose serveriuose yra negalimas. Prašome įdiegti lokalioje aplinkoje arba įgalinkite jį su panel.install opcija.", + "installation.issues.accounts": + "Katalogas /site/accounts neegzistuoja arba neturi įrašymo teisių", + "installation.issues.content": + "Katalogas /content neegzistuoja arba neturi įrašymo teisių", + "installation.issues.curl": "Plėtinys CURL yra privalomas", + "installation.issues.headline": "Nepavyko įdiegti valdymo pulto", + "installation.issues.mbstring": + "Plėtinys MB String yra privalomas", + "installation.issues.media": + "Katalogas /media neegzistuoja arba neturi įrašymo teisių", + "installation.issues.php": "Įsitikinkite, kad naudojama PHP 7+", + "installation.issues.server": + "Kirby reikalauja Apache, Nginx arba Caddy", + "installation.issues.sessions": "Katalogas /site/sessions neegzistuoja arba neturi įrašymo teisių", + + "language": "Kalba", + "language.code": "Kodas", + "language.convert": "Padaryti pagrindinį", + "language.convert.confirm": + "

Do you really want to convert {name} to the default language? This cannot be undone.

If {name} has untranslated content, there will no longer be a valid fallback and parts of your site might be empty.

", + "language.create": "Pridėti naują kalbą", + "language.delete.confirm": + "Ar tikrai norite pašalinti {name} kalbą, kartu su visais vertimais? Grąžinti nebus įmanoma! 🙀", + "language.deleted": "Kalba pašalinta", + "language.direction": "Skaitymo kryptis", + "language.direction.ltr": "Iš kairės į dešinę", + "language.direction.rtl": "Iš dešinės į kairę", + "language.locale": "PHP locale string", + "language.locale.warning": "Jūs naudojate pasirinktinį lokalės nustatymą. Prašome pakeisti jį faile /site/languages", + "language.name": "Pavadinimas", + "language.updated": "Kalba atnaujinta", + + "languages": "Kalbos", + "languages.default": "Pagrindinė kalba", + "languages.empty": "Dar nėra kalbų", + "languages.secondary": "Papildomos kalbos", + "languages.secondary.empty": "Dar nėra papildomų kalbų", + + "license": "Licenzija", + "license.buy": "Pirkti licenziją", + "license.register": "Registruoti", + "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", + + "link": "Nuoroda", + "link.text": "Nuorodos tekstas", + + "loading": "Kraunasi", + + "lock.unsaved": "Neišsaugoti pakeitimai", + "lock.unsaved.empty": "Nebeliko neišsaugotų pakeitimų", + "lock.isLocked": "Vartotojo {email} neišsaugoti pakeitimai", + "lock.file.isLocked": "Šį failą dabar redaguoja kitas vartotojas {email}, tad jo negalima pekeisti.", + "lock.page.isLocked": "Šį puslapį dabar redaguoja kitas vartotojas {email}, tad jo negalima pekeisti.", + "lock.unlock": "Atrakinti", + "lock.isUnlocked": "Jūsų neišsaugoti pakeitimai buvo perrašyti kito vartotojo. Galite parsisiųsti savo pakeitimus ir įkelti juos rankiniu būdu.", + + "login": "Prisijungti", + "login.remember": "Likti prisijungus", + + "logout": "Atsijungti", + + "menu": "Meniu", + "meridiem": "AM/PM", + "mime": "Media Tipas", + "minutes": "Minutės", + + "month": "Mėnuo", + "months.april": "Balandis", + "months.august": "August", + "months.december": "Gruodis", + "months.february": "Vasaris", + "months.january": "Sausis", + "months.july": "Liepa", + "months.june": "Birželis", + "months.march": "Kovas", + "months.may": "Gegužė", + "months.november": "Lapkritis", + "months.october": "Spalis", + "months.september": "Rugsėjis", + + "more": "Daugiau", + "name": "Pavadinimas", + "next": "Toliau", + "off": "off", + "on": "on", + "open": "Atidaryti", + "options": "Pasirinkimai", + + "orientation": "Orientacija", + "orientation.landscape": "Horizontali", + "orientation.portrait": "Portretas", + "orientation.square": "Kvadratas", + + "page.changeSlug": "Pakeisti URL", + "page.changeSlug.fromTitle": "Sukurti URL pagal pavadinimą", + "page.changeStatus": "Pakeisti statusą", + "page.changeStatus.position": "Pasirinkite poziciją", + "page.changeStatus.select": "Pasirinkite statusą", + "page.changeTemplate": "Pakeisti šabloną", + "page.delete.confirm": + "🙀 Ar tikrai norite pašalinti puslapį {title}?", + "page.delete.confirm.subpages": + "Šis puslapis turi sub-puslapių.
Visi sub-puslapiai taip pat bus pašalinti.", + "page.delete.confirm.title": "Įrašykite puslapio pavadinimą tam, kad patvirtinti", + "page.draft.create": "Sukurti juodraštį", + "page.duplicate.appendix": "Kopijuoti", + "page.duplicate.files": "Kopijuoti failus", + "page.duplicate.pages": "Kopijuoti puslapius", + "page.status": "Statusas", + "page.status.draft": "Juodraštis", + "page.status.draft.description": + "Puslapis nematomas viešai, bet matomas tik prisijungusiems administratoriams", + "page.status.listed": "Paskelbtas", + "page.status.listed.description": "Matomas viešai visiems", + "page.status.unlisted": "Nerodomas", + "page.status.unlisted.description": "Rodomas viešai visiems, bet tik per URL", + + "pages": "Puslapiai", + "pages.empty": "Dar nėra puslapių", + "pages.status.draft": "Juodraščiai", + "pages.status.listed": "Paskelbti", + "pages.status.unlisted": "Nerodomi", + + "pagination.page": "Puslapis", + + "password": "Slaptažodis", + "pixel": "Pikselis", + "prev": "Ankstesnis", + "remove": "Pašalinti", + "rename": "Pervadinti", + "replace": "Apkeisti", + "retry": "Bandyti dar", + "revert": "Grąžinti", + + "role": "Rolė", + "role.admin.description": "Admin turi visas teises", + "role.admin.title": "Admin", + "role.all": "Visos", + "role.empty": "Nėra vartotojų su tokia role", + "role.description.placeholder": "Be aprašymo", + "role.nobody.description": "Ši rolė bus naudojama jei nenustatytos jokios teisės", + "role.nobody.title": "Niekas", + + "save": "Išsaugoti", + "search": "Ieškoti", + + "section.required": "Sekcija privaloma", + + "select": "Pasirinkti", + "settings": "Nustatymai", + "size": "Dydis", + "slug": "URL pabaiga", + "sort": "Rikiuoti", + "title": "Pavadinimas", + "template": "Puslapio šablonas", + "today": "Šiandien", + + "toolbar.button.code": "Kodas", + "toolbar.button.bold": "Bold", + "toolbar.button.email": "El. paštas", + "toolbar.button.headings": "Antraštės", + "toolbar.button.heading.1": "Heading 1", + "toolbar.button.heading.2": "Heading 2", + "toolbar.button.heading.3": "Heading 3", + "toolbar.button.italic": "Italic", + "toolbar.button.file": "Failas", + "toolbar.button.file.select": "Pasirinkite failą", + "toolbar.button.file.upload": "Įkelti failą", + "toolbar.button.link": "Nuoroda", + "toolbar.button.ol": "Sąrašas su skaičiais", + "toolbar.button.ul": "Sąrašas su taškais", + + "translation.author": "Roman U", + "translation.direction": "ltr", + "translation.name": "Lietuvių", + "translation.locale": "lt_LT", + + "upload": "Įkelti", + "upload.error.cantMove": "Įkeltas failas negali būti perkeltas", + "upload.error.cantWrite": "Nepavyko įrašyti failo į diską", + "upload.error.default": "Nepavyko įkelti failo", + "upload.error.extension": "Neįmanoma įkelti tokio tipo failo", + "upload.error.formSize": "Įkeltas failas viršija MAX_FILE_SIZE nustatymą, kuris buvo nurodytas formoje", + "upload.error.iniPostSize": "Įkeliamas failas viršija post_max_size nustatymą iš php.ini", + "upload.error.iniSize": "Įkeltas failas viršija upload_max_filesize nustatymą faile php.ini", + "upload.error.noFile": "Failas nebuvo įkeltas", + "upload.error.noFiles": "Failai nebuvo įkelti", + "upload.error.partial": "Failas įkeltas tik iš dalies", + "upload.error.tmpDir": "Trūksta laikinojo katalogo", + "upload.errors": "Klaida", + "upload.progress": "Įkėlimas…", + + "url": "Url", + "url.placeholder": "https://example.com", + + "user": "Vartotojas", + "user.blueprint": + "Galite nustatyti papildomas sekcijas ir formos laukelius šiai vartotojo rolei faile /site/blueprints/users/{role}.yml", + "user.changeEmail": "Keisti el. paštą", + "user.changeLanguage": "Keisti kalbą", + "user.changeName": "Pervadinti vartotoją", + "user.changePassword": "Keisti slaptažodį", + "user.changePassword.new": "Naujas slaptažodis", + "user.changePassword.new.confirm": "Patvirtinti naują slaptažodį…", + "user.changeRole": "Keisti rolę", + "user.changeRole.select": "Pasirinkti naują rolę", + "user.create": "Pridėti naują vartotoją", + "user.delete": "Pašalinti šį vartotoją", + "user.delete.confirm": + "Ar tikrai norite pašalinti vartotoją
{email}?", + + "users": "Vartotojai", + + "version": "Versija", + + "view.account": "Jūsų paskyra", + "view.installation": "Installation", + "view.settings": "Nustatymai", + "view.site": "Svetainė", + "view.users": "Vartotojai", + + "welcome": "Sveiki", + "year": "Metai" +} diff --git a/kirby/i18n/translations/nb.json b/kirby/i18n/translations/nb.json new file mode 100755 index 0000000..ddeab80 --- /dev/null +++ b/kirby/i18n/translations/nb.json @@ -0,0 +1,481 @@ +{ + "add": "Legg til", + "avatar": "Profilbilde", + "back": "Tilbake", + "cancel": "Avbryt", + "change": "Endre", + "close": "Lukk", + "confirm": "Lagre", + "copy": "Kopier", + "create": "Opprett", + + "date": "Dato", + "date.select": "Velg dato", + + "day": "Dag", + "days.fri": "Fre", + "days.mon": "Man", + "days.sat": "L\u00f8r", + "days.sun": "S\u00f8n", + "days.thu": "Tor", + "days.tue": "Tir", + "days.wed": "Ons", + + "delete": "Slett", + "dimensions": "Dimensjoner", + "disabled": "Disabled", + "discard": "Forkast", + "download": "Download", + "duplicate": "Duplicate", + "edit": "Rediger", + + "dialog.files.empty": "No files to select", + "dialog.pages.empty": "No pages to select", + "dialog.users.empty": "No users to select", + + "email": "Epost", + "email.placeholder": "epost@eksempel.no", + + "error.access.login": "Ugyldig innlogging", + "error.access.panel": "Du har ikke tilgang til panelet", + "error.access.view": "You are not allowed to access this part of the panel", + + "error.avatar.create.fail": "Profilbildet kunne ikke lastes opp", + "error.avatar.delete.fail": "Profil bildet kunne ikke bli slette", + "error.avatar.dimensions.invalid": + "Vennligst hold profilbildets bredde og høyde under 3000 piksler", + "error.avatar.mime.forbidden": + "Ugyldig MIME-type", + + "error.blueprint.notFound": "Blueprint \"{name}\" kunne ikke lastes inn", + + "error.email.preset.notFound": "E-postinnstillingen \"{name}\" ble ikke funnet", + + "error.field.converter.invalid": "Ugyldig omformer \"{converter}\"", + + "error.file.changeName.empty": "The name must not be empty", + "error.file.changeName.permission": + "Du er ikke tillatt å endre navnet til \"{filename}\"", + "error.file.duplicate": "En fil med navnet \"{filename}\" eksisterer allerede", + "error.file.extension.forbidden": + "Ugyldig filtype", + "error.file.extension.missing": + "Du kan ikke laste opp filer uten filtype", + "error.file.maxheight": "The height of the image must not exceed {height} pixels", + "error.file.maxsize": "The file is too large", + "error.file.maxwidth": "The width of the image must not exceed {width} pixels", + "error.file.mime.differs": + "Den opplastede filen må være av samme MIME-type \"{mime}\"", + "error.file.mime.forbidden": "Mediatypen \"{mime}\" er ikke tillatt", + "error.file.mime.invalid": "Invalid mime type: {mime}", + "error.file.mime.missing": + "Mediatypen for \"{filename}\" kan ikke gjenkjennes", + "error.file.minheight": "The height of the image must be at least {height} pixels", + "error.file.minsize": "The file is too small", + "error.file.minwidth": "The width of the image must be at least {width} pixels", + "error.file.name.missing": "Filnavnet kan ikke være tomt", + "error.file.notFound": "Filen kunne ikke bli funnet", + "error.file.orientation": "The orientation of the image must be \"{orientation}\"", + "error.file.type.forbidden": "Du har ikke lov til å laste opp filer av typen {type}", + "error.file.undefined": "Filen kunne ikke bli funnet", + + "error.form.incomplete": "Vennligst fiks alle feil…", + "error.form.notSaved": "Skjemaet kunne ikke lagres", + + "error.language.code": "Please enter a valid code for the language", + "error.language.duplicate": "The language already exists", + "error.language.name": "Please enter a valid name for the language", + + "error.license.format": "Please enter a valid license key", + "error.license.email": "Vennligst skriv inn en gyldig e-postadresse", + "error.license.verification": "The license could not be verified", + + "error.page.changeSlug.permission": + "Du kan ikke endre URLen for denne siden", + "error.page.changeStatus.incomplete": + "Siden har feil og kan ikke publiseres", + "error.page.changeStatus.permission": + "Sidens status kan ikke endres", + "error.page.changeStatus.toDraft.invalid": + "Siden \"{slug}\" kan ikke konverteres til et utkast", + "error.page.changeTemplate.invalid": + "Malen for siden \"{slug}\" kan ikke endres", + "error.page.changeTemplate.permission": + "Du har ikke tillatelse til å endre malen for \"{slug}\"", + "error.page.changeTitle.empty": "Tittelen kan ikke være tom", + "error.page.changeTitle.permission": + "Du har ikke tillatelse til å endre tittelen for \"{slug}\"", + "error.page.create.permission": "Du har ikke tillatelse til å opprette \"{slug}\"", + "error.page.delete": "Siden \"{slug}\" kan ikke slettes", + "error.page.delete.confirm": "Vennligst skriv inn sidens tittel for å bekrefte", + "error.page.delete.hasChildren": + "Siden har undersider og kan derfor ikke slettes", + "error.page.delete.permission": "Du har ikke tilgang til å slette \"{slug}\"", + "error.page.draft.duplicate": + "Et sideutkast med URL-tillegget \"{slug}\" eksisterer allerede", + "error.page.duplicate": + "En side med URL-tillegget \"{slug}\" eksisterer allerede", + "error.page.duplicate.permission": "You are not allowed to duplicate \"{slug}\"", + "error.page.notFound": "Siden \"{slug}\" ble ikke funnet", + "error.page.num.invalid": + "Vennligst skriv inn et gyldig sorteringsnummer. Tallet må ikke være negativt.", + "error.page.slug.invalid": "Vennligst skriv inn en gyldig URL-prefiks", + "error.page.sort.permission": "Siden \"{slug}\" kan ikke sorteres", + "error.page.status.invalid": "Vennligst angi en gyldig sidestatus", + "error.page.undefined": "Siden kunne ikke bli funnet", + "error.page.update.permission": "Du har ikke tilgang til å oppdatere \"{slug}\"", + + "error.section.files.max.plural": + "Det er ikke mulig å legge til mer enn {max} filer i seksjonen \"{section}\"", + "error.section.files.max.singular": + "Det er ikke mulig å legge til mer enn én fil i seksjonen \"{section}\"", + "error.section.files.min.plural": + "The \"{section}\" section requires at least {min} files", + "error.section.files.min.singular": + "The \"{section}\" section requires at least one file", + + "error.section.pages.max.plural": + "Det er ikke mulig å legge til mer enn {max} sider i \"{section}\" seksjonen", + "error.section.pages.max.singular": + "Det er ikke mulig å legge til mer enn én side i \"{section}\" seksjonen", + "error.section.pages.min.plural": + "The \"{section}\" section requires at least {min} pages", + "error.section.pages.min.singular": + "The \"{section}\" section requires at least one page", + + "error.section.notLoaded": "Seksjonen \"{name}\" kunne ikke lastes inn", + "error.section.type.invalid": "Seksjonstypen \"{type}\" er ikke gyldig", + + "error.site.changeTitle.empty": "Tittelen kan ikke være tom", + "error.site.changeTitle.permission": + "Du har ikke tillatelse til å endre tittel på siden", + "error.site.update.permission": "Du har ikke tillatelse til å oppdatere denne siden", + + "error.template.default.notFound": "Standardmalen eksisterer ikke", + + "error.user.changeEmail.permission": + "Du har ikke tillatelse til å endre e-post for brukeren \"{name}\"", + "error.user.changeLanguage.permission": + "Du har ikke tillatelse til å endre språk for brukeren \"{name}\"", + "error.user.changeName.permission": + "Du har ikke tillatelse til å endre navn for brukeren \"{name}\"", + "error.user.changePassword.permission": + "Du har ikke tillatelse til å endre passord for brukeren \"{name}\"", + "error.user.changeRole.lastAdmin": + "Rollen for den siste administratoren kan ikke endres", + "error.user.changeRole.permission": + "Du har ikke tillatelse til å endre rollen for brukeren \"{name}\"", + "error.user.changeRole.toAdmin": + "You are not allowed to promote someone to the admin role", + "error.user.create.permission": "Du har ikke tilgang til å opprette denne brukeren", + "error.user.delete": "Denne brukeren kunne ikke bli slettet", + "error.user.delete.lastAdmin": "Siste administrator kan ikke slettes", + "error.user.delete.lastUser": "Den siste brukeren kan ikke slettes", + "error.user.delete.permission": + "Du er ikke tillat \u00e5 slette denne brukeren", + "error.user.duplicate": + "En bruker med e-postadresse \"{email}\" eksisterer allerede", + "error.user.email.invalid": "Vennligst skriv inn en gyldig e-postadresse", + "error.user.language.invalid": "Vennligst skriv inn et gyldig språk", + "error.user.notFound": "Brukeren kunne ikke bli funnet", + "error.user.password.invalid": + "Vennligst skriv inn et gyldig passord. Passordet må minst være 8 tegn langt.", + "error.user.password.notSame": "Vennligst bekreft passordet", + "error.user.password.undefined": "Brukeren har ikke et passord", + "error.user.role.invalid": "Vennligst skriv inn en gyldig rolle", + "error.user.update.permission": + "Du har ikke tillatelse til å oppdatere brukeren \"{name}\"", + + "error.validation.accepted": "Vennligst bekreft", + "error.validation.alpha": "Vennligst skriv kun tegn mellom a-z", + "error.validation.alphanum": + "Vennligst skriv kun tegn mellom a-z eller tall mellom 0-9", + "error.validation.between": + "Vennligst angi en verdi mellom \"{min}\" og \"{max}\"", + "error.validation.boolean": "Vennligst bekreft eller avslå", + "error.validation.contains": + "Vennligst skriv inn en verdi som inneholder \"{needle}\"", + "error.validation.date": "Vennligst skriv inn en gyldig dato", + "error.validation.date.after": "Please enter a date after {date}", + "error.validation.date.before": "Please enter a date before {date}", + "error.validation.date.between": "Please enter a date between {min} and {max}", + "error.validation.denied": "Vennligst avslå", + "error.validation.different": "Verdien kan ikke være \"{other}\"", + "error.validation.email": "Vennligst skriv inn en gyldig e-postadresse", + "error.validation.endswith": "Verdien må ende med \"{end}\"", + "error.validation.filename": "Vennligst skriv inn et gyldig filnavn", + "error.validation.in": "Vennligst skriv inn en av følgende: ({in})", + "error.validation.integer": "Vennligst skriv inn et gyldig tall", + "error.validation.ip": "Vennligst skriv inn en gyldig IP-adresse", + "error.validation.less": "Vennligst angi en verdi lavere enn {max}", + "error.validation.match": "Verdien samsvarer ikke med det forventede mønsteret", + "error.validation.max": "Vennligst angi en verdi lik eller lavere enn {max}", + "error.validation.maxlength": + "Vennligst angi en kortere verdi. (maks. {max} tegn)", + "error.validation.maxwords": "Vennligst ikke skriv inn mer enn {max} ord", + "error.validation.min": "Vennligst angi en verdi lik eller større enn {min}", + "error.validation.minlength": + "Vennligst angi en lengre verdi. (minimum. {min} tegn)", + "error.validation.minwords": "Vennligst skriv inn minst {min} ord", + "error.validation.more": "Vennligst angi en verdi større enn {min}", + "error.validation.notcontains": + "Vennligst angi en verdi som ikke inneholder \"{needle}\"", + "error.validation.notin": + "Vennligst ikke angi noen av følgende:({notIn})", + "error.validation.option": "Vennligst velg et gyldig alternativ", + "error.validation.num": "Vennligst angi et gyldig nummer", + "error.validation.required": "Vennligst skriv inn noe", + "error.validation.same": "Vennligst angi \"{other}\"", + "error.validation.size": "Størrelsen på verdien må være \"{size}\"", + "error.validation.startswith": "Verdien må starte med \"{start}\"", + "error.validation.time": "Vennligst angi et gyldig tidspunkt", + "error.validation.url": "Vennligst skriv inn en gyldig URL", + + "field.required": "The field is required", + "field.files.empty": "Ingen filer har blitt valgt", + "field.pages.empty": "Ingen side har blitt valgt", + "field.structure.delete.confirm": "\u00d8nsker du virkelig \u00e5 slette denne oppf\u00f8ringen?", + "field.structure.empty": "Ingen oppf\u00f8ringer enda", + "field.users.empty": "Ingen bruker har blitt valgt", + + "file.delete.confirm": + "Vil du virkelig slette denne filen?", + + "files": "Filer", + "files.empty": "Ingen filer ennå", + + "hour": "Time", + "insert": "Sett Inn", + "install": "Installer", + + "installation": "Installasjon", + "installation.completed": "Panelet har blitt installert", + "installation.disabled": "Installasjonsprogrammet for Panelet er deaktivert på offentlige servere som standard. Vennligst kjør installasjonsprogrammet på en lokal maskin eller aktiver den med panel.install innstillingen.", + "installation.issues.accounts": + "\/site\/accounts er ikke skrivbar", + "installation.issues.content": + "Mappen content og alt av innhold m\u00e5 v\u00e6re skrivbar.", + "installation.issues.curl": "Utvidelsen CURL er nødvendig", + "installation.issues.headline": "Panelet kan ikke installeres", + "installation.issues.mbstring": + "Utvidelsen MB String er nødvendig", + "installation.issues.media": + "Mappen /media eksisterer ikke eller er ikke skrivbar", + "installation.issues.php": "Pass på at du bruker PHP 7+", + "installation.issues.server": + "Kirby krever Apache, Nginx eller Caddy", + "installation.issues.sessions": "Mappen /site/sessions eksisterer ikke eller er ikke skrivbar", + + "language": "Spr\u00e5k", + "language.code": "Kode", + "language.convert": "Gjør til standard", + "language.convert.confirm": + "

Vil du virkelig konvertere {name} til standardspråk? Dette kan ikke angres.

Dersom {name} har innhold som ikke er oversatt, vil nettstedet mangle innhold å falle tilbake på. Dette kan resultere i at deler av nettstedet fremstår som tomt.

", + "language.create": "Legg til språk", + "language.delete.confirm": + "Vil du virkelig slette språket {name} inkludert alle oversettelser? Dette kan ikke angres!", + "language.deleted": "Språket har blitt slettet", + "language.direction": "Leseretning", + "language.direction.ltr": "Venstre til høyre", + "language.direction.rtl": "Høyre til venstre", + "language.locale": "PHP locale streng", + "language.locale.warning": "You are using a custom locale set up. Please modify it in the language file in /site/languages", + "language.name": "Navn", + "language.updated": "Språk har blitt oppdatert", + + "languages": "Språk", + "languages.default": "Standardspråk", + "languages.empty": "Det er ingen språk ennå", + "languages.secondary": "Sekundære språk", + "languages.secondary.empty": "Det er ingen andre språk ennå", + + "license": "Kirby lisens", + "license.buy": "Kjøp lisens", + "license.register": "Registrer", + "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", + + "link": "Adresse", + "link.text": "Koblingstekst", + + "loading": "Laster inn", + + "lock.unsaved": "Unsaved changes", + "lock.unsaved.empty": "There are no more unsaved changes", + "lock.isLocked": "Unsaved changes by {email}", + "lock.file.isLocked": "The file is currently being edited by {email} and cannot be changed.", + "lock.page.isLocked": "The page is currently being edited by {email} and cannot be changed.", + "lock.unlock": "Unlock", + "lock.isUnlocked": "Your unsaved changes have been overwritten by another user. You can download your changes to merge them manually.", + + "login": "Logg Inn", + "login.remember": "Hold meg innlogget", + + "logout": "Logg ut", + + "menu": "Meny", + "meridiem": "AM/PM", + "mime": "Mediatype", + "minutes": "Minutter", + + "month": "Måned", + "months.april": "April", + "months.august": "August", + "months.december": "Desember", + "months.february": "Februar", + "months.january": "Januar", + "months.july": "July", + "months.june": "Juni", + "months.march": "Mars", + "months.may": "Mai", + "months.november": "November", + "months.october": "Oktober", + "months.september": "September", + + "more": "Mer", + "name": "Navn", + "next": "Neste", + "off": "off", + "on": "on", + "open": "Åpne", + "options": "Alternativer", + + "orientation": "Orientering", + "orientation.landscape": "Landskap", + "orientation.portrait": "Portrett", + "orientation.square": "Kvadrat", + + "page.changeSlug": "Endre URL", + "page.changeSlug.fromTitle": "Opprett fra tittel", + "page.changeStatus": "Endre status", + "page.changeStatus.position": "Vennligst velg en posisjon", + "page.changeStatus.select": "Velg ny status", + "page.changeTemplate": "Endre mal", + "page.delete.confirm": + "Vil du virkelig slette denne siden?", + "page.delete.confirm.subpages": + "Denne siden har undersider.
Alle undersider vil også bli slettet.", + "page.delete.confirm.title": "Skriv inn sidetittel for å bekrefte", + "page.draft.create": "Lag utkast", + "page.duplicate.appendix": "Kopier", + "page.duplicate.files": "Copy files", + "page.duplicate.pages": "Copy pages", + "page.status": "Status", + "page.status.draft": "Utkast", + "page.status.draft.description": + "Siden er i utkastmodus og bare synlig for innloggede redaktører", + "page.status.listed": "Offentlig", + "page.status.listed.description": "Siden er offentlig og synlig for alle", + "page.status.unlisted": "Unotert", + "page.status.unlisted.description": "Siden er ikke er oppført og er kun tilgjengelig via URL", + + "pages": "Sider", + "pages.empty": "Ingen sider ennå", + "pages.status.draft": "Utkast", + "pages.status.listed": "Publisert", + "pages.status.unlisted": "Unotert", + + "pagination.page": "Side", + + "password": "Passord", + "pixel": "Piksel", + "prev": "Forrige", + "remove": "Fjern", + "rename": "Endre navn", + "replace": "Erstatt", + "retry": "Pr\u00f8v p\u00e5 nytt", + "revert": "Forkast", + + "role": "Rolle", + "role.admin.description": "The admin has all rights", + "role.admin.title": "Admin", + "role.all": "Alle", + "role.empty": "Det er ingen brukere med denne rollen", + "role.description.placeholder": "Ingen beskrivelse", + "role.nobody.description": "This is a fallback role without any permissions", + "role.nobody.title": "Nobody", + + "save": "Lagre", + "search": "Søk", + + "section.required": "The section is required", + + "select": "Velg", + "settings": "Innstillinger", + "size": "Størrelse", + "slug": "URL-appendiks", + "sort": "Sortere", + "title": "Tittel", + "template": "Mal", + "today": "I dag", + + "toolbar.button.code": "Kode", + "toolbar.button.bold": "Tykk tekst", + "toolbar.button.email": "Epost", + "toolbar.button.headings": "Overskrifter", + "toolbar.button.heading.1": "Overskrift 1", + "toolbar.button.heading.2": "Overskrift 2", + "toolbar.button.heading.3": "Overskrift 3", + "toolbar.button.italic": "Kursiv tekst", + "toolbar.button.file": "Fil", + "toolbar.button.file.select": "Select a file", + "toolbar.button.file.upload": "Upload a file", + "toolbar.button.link": "Adresse", + "toolbar.button.ol": "Ordnet liste", + "toolbar.button.ul": "Punktliste", + + "translation.author": "Kirby Team", + "translation.direction": "ltr", + "translation.name": "Norsk Bokm\u00e5l", + "translation.locale": "nb_NO", + + "upload": "Last opp", + "upload.error.cantMove": "The uploaded file could not be moved", + "upload.error.cantWrite": "Failed to write file to disk", + "upload.error.default": "The file could not be uploaded", + "upload.error.extension": "File upload stopped by extension", + "upload.error.formSize": "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the form", + "upload.error.iniPostSize": "The uploaded file exceeds the post_max_size directive in php.ini", + "upload.error.iniSize": "The uploaded file exceeds the upload_max_filesize directive in php.ini", + "upload.error.noFile": "No file was uploaded", + "upload.error.noFiles": "No files were uploaded", + "upload.error.partial": "The uploaded file was only partially uploaded", + "upload.error.tmpDir": "Missing a temporary folder", + "upload.errors": "Feil", + "upload.progress": "Laster opp…", + + "url": "Nettadresse", + "url.placeholder": "https://example.com", + + "user": "Bruker", + "user.blueprint": + "Du kan definere flere seksjoner og skjemafelter for denne brukerrollen i /site/blueprints/users/{role}.yml", + "user.changeEmail": "Endre e-post", + "user.changeLanguage": "Endre språk", + "user.changeName": "Angi nytt navn for denne brukeren", + "user.changePassword": "Bytt passord", + "user.changePassword.new": "Nytt passord", + "user.changePassword.new.confirm": "Bekreft nytt passord…", + "user.changeRole": "Bytt rolle", + "user.changeRole.select": "Velg en ny rolle", + "user.create": "Legg til ny bruker", + "user.delete": "Slett denne brukeren", + "user.delete.confirm": + "Vil du virkelig slette denne konten?", + + "users": "Brukere", + + "version": "Kirby versjon", + + "view.account": "Din konto", + "view.installation": "Installasjon", + "view.settings": "Innstillinger", + "view.site": "Side", + "view.users": "Brukere", + + "welcome": "Velkommen", + "year": "År" +} diff --git a/kirby/i18n/translations/nl.json b/kirby/i18n/translations/nl.json new file mode 100755 index 0000000..2b9c1f5 --- /dev/null +++ b/kirby/i18n/translations/nl.json @@ -0,0 +1,481 @@ +{ + "add": "Voeg toe", + "avatar": "Avatar", + "back": "Terug", + "cancel": "Annuleren", + "change": "Wijzigen", + "close": "Sluiten", + "confirm": "OK", + "copy": "Kopiëren", + "create": "Aanmaken", + + "date": "Datum", + "date.select": "Selecteer een datum", + + "day": "Dag", + "days.fri": "Vr", + "days.mon": "Ma", + "days.sat": "Za", + "days.sun": "Zo", + "days.thu": "Do", + "days.tue": "Di", + "days.wed": "Wo", + + "delete": "Verwijderen", + "dimensions": "Dimensies", + "disabled": "Uitgeschakeld", + "discard": "Annuleren", + "download": "Download", + "duplicate": "Dupliceren", + "edit": "Wijzig", + + "dialog.files.empty": "Geen bestanden om te selecteren", + "dialog.pages.empty": "Geen pagina's om te selecteren", + "dialog.users.empty": "Geen gebruikers om te selecteren", + + "email": "E-mailadres", + "email.placeholder": "mail@voorbeeld.nl", + + "error.access.login": "Ongeldige login", + "error.access.panel": "Je hebt geen toegang tot het Panel", + "error.access.view": "Je hebt geen toegangsrechten voor deze zone van het Panel", + + "error.avatar.create.fail": "De avatar kon niet worden geupload", + "error.avatar.delete.fail": "De avatar kan niet worden verwijderd", + "error.avatar.dimensions.invalid": + "Houd de breedte en hoogte van de avatar onder 3000 pixels", + "error.avatar.mime.forbidden": + "De avatar moet een JPEG of PNG bestand zijn", + + "error.blueprint.notFound": "De blueprint \"{name}\" kon niet geladen worden", + + "error.email.preset.notFound": "De e-mailvoorinstelling \"{name}\" kan niet worden gevonden", + + "error.field.converter.invalid": "Ongeldige converter \"{converter}\"", + + "error.file.changeName.empty": "De naam mag niet leeg zijn", + "error.file.changeName.permission": + "Je hebt geen rechten om de naam te wijzigen van \"{filename}\"", + "error.file.duplicate": "Er bestaat al een bestand met de naam \"{filename}\"", + "error.file.extension.forbidden": + "Bestandsextensie \"{extension}\" is niet toegestaan", + "error.file.extension.missing": + "Je kunt geen bestanden uploaden zonder bestandsextensie", + "error.file.maxheight": "The height of the image must not exceed {height} pixels", + "error.file.maxsize": "The file is too large", + "error.file.maxwidth": "The width of the image must not exceed {width} pixels", + "error.file.mime.differs": + "Het geüploade bestand moet van hetzelfde mime-type zijn: \"{mime}\"", + "error.file.mime.forbidden": "Het type \"{mime}\" is niet toegestaan", + "error.file.mime.invalid": "Invalid mime type: {mime}", + "error.file.mime.missing": + "Het mediatype voor \"{filename}\" kan niet worden gedecteerd", + "error.file.minheight": "The height of the image must be at least {height} pixels", + "error.file.minsize": "The file is too small", + "error.file.minwidth": "The width of the image must be at least {width} pixels", + "error.file.name.missing": "De bestandsnaam mag niet leeg zijn", + "error.file.notFound": "Het bestand kan niet worden gevonden", + "error.file.orientation": "The orientation of the image must be \"{orientation}\"", + "error.file.type.forbidden": "Je hebt geen rechten om {type} bestanden up te loaden", + "error.file.undefined": "Het bestand kan niet worden gevonden", + + "error.form.incomplete": "Verbeter alle fouten in het formulier", + "error.form.notSaved": "Het formulier kon niet worden opgeslagen", + + "error.language.code": "Vul een geldige code voor deze taal in", + "error.language.duplicate": "De taal bestaat al", + "error.language.name": "Vul een geldige naam voor deze taal in", + + "error.license.format": "Vul een gelidge licentie-key in", + "error.license.email": "Gelieve een geldig emailadres in te voeren", + "error.license.verification": "De licentie kon niet worden geverifieerd. ", + + "error.page.changeSlug.permission": + "Je kunt de URL van deze pagina niet wijzigen", + "error.page.changeStatus.incomplete": + "Deze pagina bevat fouten en kan niet worden gepubliceerd", + "error.page.changeStatus.permission": + "De status van deze pagina kan niet worden gewijzigd", + "error.page.changeStatus.toDraft.invalid": + "De pagina \"{slug}\" kan niet worden aangepast naar 'concept'", + "error.page.changeTemplate.invalid": + "De template van deze pagina \"{slug}\" kan niet worden gewijzigd", + "error.page.changeTemplate.permission": + "Je hebt geen rechten om het template te wijzigen van \"{slug}\"", + "error.page.changeTitle.empty": "De titel mag niet leeg zijn", + "error.page.changeTitle.permission": + "Je hebt geen rechten om de titel te wijzigen van \"{slug}\"", + "error.page.create.permission": "Je hebt geen rechten om \"{slug}\" aan te maken", + "error.page.delete": "De pagina \"{slug}\" kan niet worden verwijderd", + "error.page.delete.confirm": "Voer de paginatitel in om te bevestigen", + "error.page.delete.hasChildren": + "Deze pagina heeft subpagina's en kan niet worden verwijderd", + "error.page.delete.permission": "Je hebt geen rechten om \"{slug}\" te verwijderen", + "error.page.draft.duplicate": + "Er bestaat al een conceptpagina met de URL-appendix \"{slug}\"", + "error.page.duplicate": + "Er bestaat al een pagina met de URL-appendix \"{slug}\"", + "error.page.duplicate.permission": "Je bent niet gemachtigd om \"{slug}\" te dupliceren", + "error.page.notFound": "De pagina \"{slug}\" kan niet worden gevonden", + "error.page.num.invalid": + "Vul een geldig sorteer-cijfer in. Het cijfer mag niet negatief zijn", + "error.page.slug.invalid": "Vul een geldige URL-prefix in", + "error.page.sort.permission": "De pagina \"{slug}\" kan niet worden gesorteerd", + "error.page.status.invalid": "Zorg voor een geldige paginastatus", + "error.page.undefined": "De pagina kan niet worden gevonden", + "error.page.update.permission": "Je hebt geen rechten om \"{slug}\" te updaten", + + "error.section.files.max.plural": + "Voeg niet meer dan {max} bestanden toe aan de zone \"{section}\"", + "error.section.files.max.singular": + "Je kunt niet meer dan 1 bestand toevoegen aan de zone \"{section}\"", + "error.section.files.min.plural": + "De \"{section}\" sectie moet minimaal {min} bestanden bevatten.", + "error.section.files.min.singular": + "De \"{section}\" sectie moet minimaal 1 bestand bevatten.", + + "error.section.pages.max.plural": + "Je kunt niet meer dan {max} pagina's toevoegen aan de zone \"{section}\"", + "error.section.pages.max.singular": + "Je kunt niet meer dan 1 pagina toevoegen aan de zone \"{section}\"", + "error.section.pages.min.plural": + "De \"{section}\" sectie moet minimaal {min} pagina's bevatten.", + "error.section.pages.min.singular": + "De \"{section}\" sectie moet minimaal 1 pagina bevatten.", + + "error.section.notLoaded": "De zone \"{name}\" kan niet worden geladen", + "error.section.type.invalid": "Zone-type \"{type}\" is niet geldig", + + "error.site.changeTitle.empty": "De titel mag niet leeg zijn", + "error.site.changeTitle.permission": + "Je hebt geen rechten om de titel van de site te wijzigen", + "error.site.update.permission": "Je hebt geen rechten om de site te updaten", + + "error.template.default.notFound": "Het standaard template bestaat niet", + + "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", + "error.user.changeName.permission": + "Je hebt geen rechten om de naam van gebruiker \"{name}\" te wijzigen", + "error.user.changePassword.permission": + "Je hebt geen rechten om het wachtwoord van gebruiker \"{name}\" te wijzigen", + "error.user.changeRole.lastAdmin": + "De rol van de laatste beheerder kan niet worden gewijzigd", + "error.user.changeRole.permission": + "Je hebt geen rechten om de rol van gebruiker \"{name}\" te wijzigen", + "error.user.changeRole.toAdmin": + "Je hebt geen rechten om de rol van iemand te wijzigen naar admin", + "error.user.create.permission": "Je hebt geen rechten om deze gebruiker aan te maken", + "error.user.delete": "De gebruiker \"{name}\" kan niet worden verwijderd", + "error.user.delete.lastAdmin": "Je kan de laatste admin niet verwijderen", + "error.user.delete.lastUser": "De laatste gebruiker kan niet worden verwijderd", + "error.user.delete.permission": + "Je hebt geen rechten om gebruiker \"{name}\" te verwijderen", + "error.user.duplicate": + "Er bestaat al een gebruiker met e-mailadres \"{email}\"", + "error.user.email.invalid": "Gelieve een geldig emailadres in te voeren", + "error.user.language.invalid": "Gelieve een geldige taal in te voeren", + "error.user.notFound": "De gebruiker \"{name}\" kan niet worden gevonden", + "error.user.password.invalid": + "Gelieve een geldig wachtwoord in te voeren. Wachtwoorden moeten minstens 8 karakters lang zijn.", + "error.user.password.notSame": "De wachtwoorden komen niet overeen", + "error.user.password.undefined": "De gebruiker heeft geen wachtwoord", + "error.user.role.invalid": "Gelieve een geldige rol in te voeren", + "error.user.update.permission": + "Je hebt geen rechten om gebruiker \"{name}\" te updaten", + + "error.validation.accepted": "Gelieve te bevestigen", + "error.validation.alpha": "Vul alleen a-z karakters in", + "error.validation.alphanum": + "Vul alleen a-z karakters of cijfers (0-9) in", + "error.validation.between": + "Vul een waarde tussen \"{min}\" en \"{max}\"", + "error.validation.boolean": "Ga akkoord of weiger", + "error.validation.contains": + "Vul een waarde in die \"{needle}\" bevat", + "error.validation.date": "Vul een geldige datum in", + "error.validation.date.after": "Vul een datum in na {date}", + "error.validation.date.before": "Vul een datum in voor {date}", + "error.validation.date.between": "Vul een datum in tussen {min} en {max}", + "error.validation.denied": "Weiger", + "error.validation.different": "De invoer mag niet \"{other}\" zijn", + "error.validation.email": "Gelieve een geldig emailadres in te voeren", + "error.validation.endswith": "De invoer moet eindigen met \"{end}\"", + "error.validation.filename": "Vul een geldige bestandsnaam in", + "error.validation.in": "Vul één van de volgende dingen in: ({in})", + "error.validation.integer": "Vul een geldig geheel getal in", + "error.validation.ip": "Vul een geldig IP-adres in", + "error.validation.less": "Vul een waarde in lager dan {max}", + "error.validation.match": "De invoer klopt niet met het verwachte patroon", + "error.validation.max": "Vul een waarde in die gelijk is aan of lager dan {max}", + "error.validation.maxlength": + "Gebruik minder karakters (maximaal {max} karakters)", + "error.validation.maxwords": "Vul minder dan {max} woorden in", + "error.validation.min": "Vul een waarde in die gelijk is aan of groter dan {min}", + "error.validation.minlength": + "Gebruik meer karakters (minimaal {min} karakters)", + "error.validation.minwords": "Vul minimaal {min} woorden in", + "error.validation.more": "Vul een grotere waarde in dan {min}", + "error.validation.notcontains": + "Zorg dat de invoer niet \"{needle}\" bevat", + "error.validation.notin": + "Vul de volgende dingen niet in: {{notIn}}", + "error.validation.option": "Selecteer een geldige optie", + "error.validation.num": "Vul een geldig cijfer in", + "error.validation.required": "Vul iets in", + "error.validation.same": "Vul \"{other}\" in", + "error.validation.size": "De lengte van de invoer moet \"{size}\" zijn", + "error.validation.startswith": "De invoer moet beginnen met \"{start}\"", + "error.validation.time": "Vul een geldige tijd in", + "error.validation.url": "Vul een geldige URL in", + + "field.required": "Dit veld is verplicht", + "field.files.empty": "Nog geen bestanden geselecteerd", + "field.pages.empty": "Nog geen pagina's geselecteerd", + "field.structure.delete.confirm": "Wil je deze entry verwijderen?", + "field.structure.empty": "Nog geen items.", + "field.users.empty": "Nog geen gebruikers geselecteerd", + + "file.delete.confirm": + "Wil je dit bestand
{filename} verwijderen?", + + "files": "Bestanden", + "files.empty": "Nog geen bestanden", + + "hour": "Uur", + "insert": "Toevoegen", + "install": "Installeren", + + "installation": "Installatie", + "installation.completed": "Het Panel is geïnstalleerd", + "installation.disabled": "Je kan geen Panel installatie uitvoeren op een openbare server. Voer het installatieprogramma uit op een lokale computer of schakel het in met de panel.install optie.", + "installation.issues.accounts": + "De map /site/accounts heeft geen schrijfrechten", + "installation.issues.content": + "De map /content bestaat niet of heeft geen schrijfrechten", + "installation.issues.curl": "De CURL-extensie is vereist", + "installation.issues.headline": "Het Panel kan niet worden geïnstalleerd", + "installation.issues.mbstring": + "De MB String extensie is verplicht", + "installation.issues.media": + "De map /mediabestaat niet of heeft geen schrijfrechten", + "installation.issues.php": "Gebruik PHP7+", + "installation.issues.server": + "Kirby vereist Apache, Nginxof Caddy", + "installation.issues.sessions": "De map /site/sessions bestaat niet of heeft geen schrijfrechten", + + "language": "Taal", + "language.code": "Code", + "language.convert": "Maak standaard", + "language.convert.confirm": + "

Weet je zeker dat je {name}wilt aanpassen naar de standaard taal? Dit kan niet ongedaan worden gemaakt

Als {name} nog niet vertaalde content heeft, is er geen content meer om op terug te vallen en zouden delen van je site leeg kunnen zijn.

", + "language.create": "Nieuwe taal toevoegen", + "language.delete.confirm": + "Weet je zeker dat je de taal {name} inclusief alle vertalingen wilt verwijderen? Je kunt dit niet ongedaan maken!", + "language.deleted": "De taal is verwijderd", + "language.direction": "Leesrichting", + "language.direction.ltr": "Links naar rechts", + "language.direction.rtl": "Rechts naar links", + "language.locale": "PHP-locale regel", + "language.locale.warning": "You are using a custom locale set up. Please modify it in the language file in /site/languages", + "language.name": "Naam", + "language.updated": "De taal is geüpdatet", + + "languages": "Talen", + "languages.default": "Standaard taal", + "languages.empty": "Er zijn nog geen talen", + "languages.secondary": "Andere talen", + "languages.secondary.empty": "Er zijn nog geen andere talen beschikbaar", + + "license": "Licentie", + "license.buy": "Koop een licentie", + "license.register": "Registreren", + "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", + + "link": "Link", + "link.text": "Linktekst", + + "loading": "Laden", + + "lock.unsaved": "Niet opgeslagen wijzigingen", + "lock.unsaved.empty": "Er zijn geen niet opgeslagen wijzigingen meer", + "lock.isLocked": "Niet opgeslagen wijzigingen door {email}", + "lock.file.isLocked": "Dit bestand wordt momenteel bewerkt door {email} en kan niet worden gewijzigd.", + "lock.page.isLocked": "Deze pagina wordt momenteel bewerkt door {email} en kan niet worden gewijzigd.", + "lock.unlock": "Ontgrendelen", + "lock.isUnlocked": "Je niet opgeslagen wijzigingen zijn overschreven door een andere gebruiker. Je kunt je wijzigingen downloaden om ze handmatig samen te voegen.", + + "login": "Inloggen", + "login.remember": "Houd mij ingelogd", + + "logout": "Uitloggen", + + "menu": "Menu", + "meridiem": "AM/PM", + "mime": "Mime-type", + "minutes": "Minuten", + + "month": "Maand", + "months.april": "april", + "months.august": "augustus", + "months.december": "december", + "months.february": "februari", + "months.january": "januari", + "months.july": "juli", + "months.june": "juni", + "months.march": "maart", + "months.may": "mei", + "months.november": "november", + "months.october": "oktober", + "months.september": "september", + + "more": "Meer", + "name": "Naam", + "next": "Volgende", + "off": "uit", + "on": "aan", + "open": "Open", + "options": "Opties", + + "orientation": "Oriëntatie", + "orientation.landscape": "Liggend", + "orientation.portrait": "Staand", + "orientation.square": "Vierkant", + + "page.changeSlug": "Verander URL", + "page.changeSlug.fromTitle": "Aanmaken op basis van titel", + "page.changeStatus": "Wijzig status", + "page.changeStatus.position": "Selecteer een positie", + "page.changeStatus.select": "Selecteer een nieuwe status", + "page.changeTemplate": "Verander template", + "page.delete.confirm": + "Weet je zeker dat je pagina {title} wilt verwijderen?", + "page.delete.confirm.subpages": + "Deze pagina heeft subpagina's.
Alle subpagina's worden ook verwijderd.", + "page.delete.confirm.title": "Voeg een paginatitel in om te bevestigen", + "page.draft.create": "Maak concept", + "page.duplicate.appendix": "Kopiëren", + "page.duplicate.files": "Kopieer bestanden", + "page.duplicate.pages": "Kopieer pagina's", + "page.status": "Status", + "page.status.draft": "Concept", + "page.status.draft.description": + "De pagina is in concept-modus en alleen zichtbaar voor ingelogde redacteuren", + "page.status.listed": "Openbaar", + "page.status.listed.description": "Deze pagina is toegankelijk voor iedereen", + "page.status.unlisted": "Niet gepubliceerd", + "page.status.unlisted.description": "Deze pagina is alleen bereikbaar via URL", + + "pages": "Pagina’s", + "pages.empty": "Nog geen pagina's", + "pages.status.draft": "Concepten", + "pages.status.listed": "Gepubliceerd", + "pages.status.unlisted": "Niet gepubliceerd", + + "pagination.page": "Pagina", + + "password": "Wachtwoord", + "pixel": "Pixel", + "prev": "Vorige", + "remove": "Verwijder", + "rename": "Hernoem", + "replace": "Vervang", + "retry": "Probeer opnieuw", + "revert": "Annuleren", + + "role": "Rol", + "role.admin.description": "De admin heeft alle rechten", + "role.admin.title": "Admin", + "role.all": "Alle", + "role.empty": "Er zijn geen gebruikers met deze rol", + "role.description.placeholder": "Geen beschrijving", + "role.nobody.description": "Dit is een fallback-rol zonder rechten", + "role.nobody.title": "Niemand", + + "save": "Opslaan", + "search": "Zoeken", + + "section.required": "De sectie is verplicht", + + "select": "Selecteren", + "settings": "Opties", + "size": "Grootte", + "slug": "URL-toevoeging", + "sort": "Sorteren", + "title": "Titel", + "template": "Template", + "today": "Vandaag", + + "toolbar.button.code": "Code", + "toolbar.button.bold": "Dikgedrukte tekst", + "toolbar.button.email": "E-mailadres", + "toolbar.button.headings": "Titels", + "toolbar.button.heading.1": "Titel 1", + "toolbar.button.heading.2": "Titel 2", + "toolbar.button.heading.3": "Titel 3", + "toolbar.button.italic": "Cursieve tekst", + "toolbar.button.file": "Bestand", + "toolbar.button.file.select": "Selecteer een bestand", + "toolbar.button.file.upload": "Upload bestand", + "toolbar.button.link": "Link", + "toolbar.button.ol": "Genummerde lijst", + "toolbar.button.ul": "Opsomming", + + "translation.author": "Het team van Kirby", + "translation.direction": "ltr", + "translation.name": "Nederlands", + "translation.locale": "nl_NL", + + "upload": "Upload", + "upload.error.cantMove": "Het geüploadde bestand kon niet worden verplaatst", + "upload.error.cantWrite": "Fout bij het schrijven van het bestand naar de schijf", + "upload.error.default": "Het bestand kan niet worden geüpload", + "upload.error.extension": "Kan bestand niet uploaden vanwege de extensie", + "upload.error.formSize": "Het geüploadde bestand is groter dan de MAX_FILE_SIZE die is aangegeven in het formulier", + "upload.error.iniPostSize": "Het geüploadde bestand is groter dan de post_max_size in php.ini", + "upload.error.iniSize": "Het geüploadde bestand is groter dan de upload_max_filesize in php.ini", + "upload.error.noFile": "Er is geen bestand geüpload", + "upload.error.noFiles": "Er zijn geen bestanden geüpload", + "upload.error.partial": "Het geüploadde bestand is slechts gedeeltelijk geüpload", + "upload.error.tmpDir": "Er mist een tijdelijke map", + "upload.errors": "Foutmelding", + "upload.progress": "Uploaden...", + + "url": "Url", + "url.placeholder": "https://voorbeeld.nl", + + "user": "Gebruiker", + "user.blueprint": + "Je kunt extra zones en formuliervelden voor deze rol toevoegen in /site/blueprints/users/{role}.yml", + "user.changeEmail": "Email veranderen", + "user.changeLanguage": "Taal veranderen", + "user.changeName": "Gebruiker hernoemen", + "user.changePassword": "Wachtwoord wijzigen", + "user.changePassword.new": "Nieuw wachtwoord", + "user.changePassword.new.confirm": "Bevestig het nieuwe wachtwoord...", + "user.changeRole": "Verander rol", + "user.changeRole.select": "Kies een nieuwe rol", + "user.create": "Voeg een nieuwe gebruiker toe", + "user.delete": "Verwijder deze gebruiker", + "user.delete.confirm": + "Weet je zeker dat je
{email}wil verwijderen?", + + "users": "Gebruikers", + + "version": "Kirby-versie", + + "view.account": "Jouw account", + "view.installation": "Installatie", + "view.settings": "Opties", + "view.site": "Site", + "view.users": "Gebruikers", + + "welcome": "Welkom", + "year": "Jaar" +} diff --git a/kirby/i18n/translations/pl.json b/kirby/i18n/translations/pl.json new file mode 100755 index 0000000..66c2c06 --- /dev/null +++ b/kirby/i18n/translations/pl.json @@ -0,0 +1,481 @@ +{ + "add": "Dodaj", + "avatar": "Zdj\u0119cie profilowe", + "back": "Wróć", + "cancel": "Anuluj", + "change": "Zmie\u0144", + "close": "Zamknij", + "confirm": "Ok", + "copy": "Kopiuj", + "create": "Utwórz", + + "date": "Data", + "date.select": "Wybierz datę", + + "day": "Dzień", + "days.fri": "Pt", + "days.mon": "Pn", + "days.sat": "Sb", + "days.sun": "Nd", + "days.thu": "Czw", + "days.tue": "Wt", + "days.wed": "\u015ar", + + "delete": "Usu\u0144", + "dimensions": "Wymiary", + "disabled": "Disabled", + "discard": "Odrzu\u0107", + "download": "Pobierz", + "duplicate": "Zduplikuj", + "edit": "Edytuj", + + "dialog.files.empty": "No files to select", + "dialog.pages.empty": "No pages to select", + "dialog.users.empty": "No users to select", + + "email": "Email", + "email.placeholder": "mail@example.com", + + "error.access.login": "Nieprawidłowy login", + "error.access.panel": "Nie masz uprawnień by dostać się do panelu", + "error.access.view": "You are not allowed to access this part of the panel", + + "error.avatar.create.fail": "Nie udało się załadować zdjęcia profilowego", + "error.avatar.delete.fail": "Nie udało się usunąć zdjęcia profilowego", + "error.avatar.dimensions.invalid": + "Proszę zachować szerokość i wysokość zdjęcia profilowego poniżej 3000 pikseli", + "error.avatar.mime.forbidden": + "Zdjęcie profilowe musi być plikiem JPEG lub PNG", + + "error.blueprint.notFound": "Nie udało się załadować wzorca \"{name}\"", + + "error.email.preset.notFound": "Nie udało się załadować wzorca wiadomości e-mail \"{name}\"", + + "error.field.converter.invalid": "Nieprawidłowy konwerter \"{converter}\"", + + "error.file.changeName.empty": "The name must not be empty", + "error.file.changeName.permission": + "Nie masz uprawnień, by zmienić nazwę \"{filename}\"", + "error.file.duplicate": "Istnieje już plik o nazwie \"{filename}\"", + "error.file.extension.forbidden": + "Rozszerzenie \"{extension}\" jest niedozwolone", + "error.file.extension.missing": + "Brak rozszerzenia pliku \"{filename}\"", + "error.file.maxheight": "The height of the image must not exceed {height} pixels", + "error.file.maxsize": "The file is too large", + "error.file.maxwidth": "The width of the image must not exceed {width} pixels", + "error.file.mime.differs": + "Przesłany plik musi być tego samego typu mime \"{mime}\"", + "error.file.mime.forbidden": "Typ multimediów \"{mime}\" jest niedozwolony", + "error.file.mime.invalid": "Invalid mime type: {mime}", + "error.file.mime.missing": + "Nie można wykryć typu multimediów dla \"{filename}\"", + "error.file.minheight": "The height of the image must be at least {height} pixels", + "error.file.minsize": "The file is too small", + "error.file.minwidth": "The width of the image must be at least {width} pixels", + "error.file.name.missing": "Nazwa pliku nie może być pusta", + "error.file.notFound": "Nie można znaleźć pliku \"{filename}\"", + "error.file.orientation": "The orientation of the image must be \"{orientation}\"", + "error.file.type.forbidden": "Nie możesz przesyłać plików {type}", + "error.file.undefined": "Nie można znaleźć pliku", + + "error.form.incomplete": "Popraw wszystkie błędy w formularzu…", + "error.form.notSaved": "Nie udało się zapisać formularza", + + "error.language.code": "Please enter a valid code for the language", + "error.language.duplicate": "The language already exists", + "error.language.name": "Please enter a valid name for the language", + + "error.license.format": "Wprowadź poprawny klucz licencyjny", + "error.license.email": "Wprowadź poprawny adres email", + "error.license.verification": "Nie udało się zweryfikować licencji", + + "error.page.changeSlug.permission": + "Nie możesz zmienić końcówki adresu URL w \"{slug}\"", + "error.page.changeStatus.incomplete": + "Strona zawiera błędy i nie można jej opublikować", + "error.page.changeStatus.permission": + "Status tej strony nie może zostać zmieniony", + "error.page.changeStatus.toDraft.invalid": + "Strony \"{slug}\" nie można przekonwertować na szkic", + "error.page.changeTemplate.invalid": + "Nie można zmienić szablonu strony \"{slug}\"", + "error.page.changeTemplate.permission": + "Nie masz uprawnień, by zmienić szablon dla \"{slug}\"", + "error.page.changeTitle.empty": "Tytuł nie może być pusty", + "error.page.changeTitle.permission": + "Nie masz uprawnień, by zmienić tytuł dla \"{slug}\"", + "error.page.create.permission": "Nie masz uprawnień, by utworzyć \"{slug}\"", + "error.page.delete": "Strony \"{slug}\" nie można usunąć", + "error.page.delete.confirm": "Wprowadź tytuł strony, aby potwierdzić", + "error.page.delete.hasChildren": + "Strona zawiera podstrony i nie można jej usunąć", + "error.page.delete.permission": "Nie masz uprawnień, by usunąć \"{slug}\"", + "error.page.draft.duplicate": + "Istnieje już szkic z końcówką URL \"{slug}\"", + "error.page.duplicate": + "Istnieje już strona z końcówką URL \"{slug}\"", + "error.page.duplicate.permission": "Nie masz uprawnień, by zduplikować \"{slug}\"", + "error.page.notFound": "Nie można znaleźć strony \"{slug}\"", + "error.page.num.invalid": + "Wprowadź poprawny numer sortujący. Liczby nie mogą być ujemne.", + "error.page.slug.invalid": "Wprowadź poprawną końcówkę adresu URL", + "error.page.sort.permission": "Nie można sortować strony \"{slug}\"", + "error.page.status.invalid": "Ustaw prawidłowy status strony", + "error.page.undefined": "Nie udało się znaleźć strony", + "error.page.update.permission": "Nie masz uprawnień, by zaktualizować \"{slug}\"", + + "error.section.files.max.plural": + "Do sekcji \"{section}\" można dodać nie więcej niż {max} plików", + "error.section.files.max.singular": + "Do sekcji \"{section}\" można dodać tylko jeden plik", + "error.section.files.min.plural": + "The \"{section}\" section requires at least {min} files", + "error.section.files.min.singular": + "The \"{section}\" section requires at least one file", + + "error.section.pages.max.plural": + "Do sekcji \"{section}\" można dodać nie więcej niż {max} stron", + "error.section.pages.max.singular": + "Do sekcji \"{section}\" można dodać tylko jedną stronę", + "error.section.pages.min.plural": + "The \"{section}\" section requires at least {min} pages", + "error.section.pages.min.singular": + "The \"{section}\" section requires at least one page", + + "error.section.notLoaded": "Nie udało się załadować sekcji \"{name}\"", + "error.section.type.invalid": "Typ sekcji \"{type}\" jest nieprawidłowy", + + "error.site.changeTitle.empty": "Tytuł nie może być pusty", + "error.site.changeTitle.permission": + "Nie masz uprawnień, by zmienić tytuł strony", + "error.site.update.permission": "Nie masz uprawnień, by zaktualizować stronę", + + "error.template.default.notFound": "Domyślny szablon nie istnieje", + + "error.user.changeEmail.permission": + "Nie masz uprawnień, by zmienić adres e-mail użytkownika \"{name}\"", + "error.user.changeLanguage.permission": + "Nie masz uprawnień, by zmienić język użytkownika \"{name}\"", + "error.user.changeName.permission": + "Nie masz uprawnień, by zmienić nazwę użytkownika \"{name}\"", + "error.user.changePassword.permission": + "Nie masz uprawnień, by zmienić hasło użytkownika \"{name}\"", + "error.user.changeRole.lastAdmin": + "Nie można zmienić roli ostatniego administratora", + "error.user.changeRole.permission": + "Nie masz uprawnień, by zmienić rolę użytkownika \"{name}\"", + "error.user.changeRole.toAdmin": + "You are not allowed to promote someone to the admin role", + "error.user.create.permission": "Nie masz uprawnień, by utworzyć tego użytkownika", + "error.user.delete": "Nie można usunąć użytkownika \"{name}\"", + "error.user.delete.lastAdmin": "Nie można usunąć ostatniego administratora", + "error.user.delete.lastUser": "Nie można usunąć ostatniego użytkownika", + "error.user.delete.permission": + "Nie masz uprawnień, by usunąć użytkownika \"{name}\"", + "error.user.duplicate": + "Istnieje już użytkownik z adresem email \"{email}\"", + "error.user.email.invalid": "Wprowadź poprawny adres email", + "error.user.language.invalid": "Proszę podać poprawny język", + "error.user.notFound": "Nie można znaleźć użytkownika \"{name}\"", + "error.user.password.invalid": + "Wprowadź prawidłowe hasło. Hasła muszą mieć co najmniej 8 znaków.", + "error.user.password.notSame": "Hasła nie są takie same", + "error.user.password.undefined": "Użytkownik nie ma hasła", + "error.user.role.invalid": "Wprowadź poprawną rolę", + "error.user.update.permission": + "Nie masz uprawnień, by zaktualizować użytkownika \"{name}\"", + + "error.validation.accepted": "Proszę potwierdzić", + "error.validation.alpha": "Wprowadź tylko znaki między a-z", + "error.validation.alphanum": + "Wprowadź tylko znaki między a-z lub cyfry 0-9", + "error.validation.between": + "Wprowadź wartość między \"{min}\" i \"{max}\"", + "error.validation.boolean": "Potwierdź lub odmów", + "error.validation.contains": + "Wprowadź wartość, która zawiera \"{needle}\"", + "error.validation.date": "Wprowadź poprawną datę", + "error.validation.date.after": "Please enter a date after {date}", + "error.validation.date.before": "Please enter a date before {date}", + "error.validation.date.between": "Please enter a date between {min} and {max}", + "error.validation.denied": "Proszę odmówić", + "error.validation.different": "Wartością nie może być \"{other}\"", + "error.validation.email": "Wprowadź poprawny adres email", + "error.validation.endswith": "Wartość musi kończyć się na \"{end}\"", + "error.validation.filename": "Wprowadź poprawną nazwę pliku", + "error.validation.in": "Wprowadź jedno z następujących: ({in})", + "error.validation.integer": "Wprowadź poprawną liczbę całkowitą", + "error.validation.ip": "Wprowadź poprawny adres IP", + "error.validation.less": "Wprowadź wartość mniejszą niż {max}", + "error.validation.match": "Wartość nie jest zgodna z oczekiwanym wzorcem", + "error.validation.max": "Wprowadź wartość równą lub mniejszą niż {max}", + "error.validation.maxlength": + "Wprowadź krótszą wartość. (maks. {max} znaków)", + "error.validation.maxwords": "Wprowadź nie więcej niż {max} słowa/słów", + "error.validation.min": "Wprowadź wartość równą lub większą niż {min}", + "error.validation.minlength": + "Wprowadź dłuższą wartość. (min. {min} znaków)", + "error.validation.minwords": "Wprowadź co najmniej {min} słowa/słów", + "error.validation.more": "Wprowadź wartość większą niż {min}", + "error.validation.notcontains": + "Wprowadź wartość, która nie zawiera \"{needle}\"", + "error.validation.notin": + "Nie wprowadzaj żadnego z następujących ({notIn})", + "error.validation.option": "Wybierz poprawną opcję", + "error.validation.num": "Wprowadź poprawny numer", + "error.validation.required": "Wpisz coś", + "error.validation.same": "Wprowadź \"{other}\"", + "error.validation.size": "Rozmiar wartości musi wynosić \"{size}\"", + "error.validation.startswith": "Wartość musi zaczynać się od \"{start}\"", + "error.validation.time": "Wprowadź poprawny czas", + "error.validation.url": "Wprowadź poprawny adres URL", + + "field.required": "The field is required", + "field.files.empty": "Nie wybrano jeszcze żadnych plików", + "field.pages.empty": "Nie wybrano jeszcze żadnych stron", + "field.structure.delete.confirm": "Czy na pewno chcesz usunąć ten wiersz?", + "field.structure.empty": "Nie ma jeszcze \u017cadnych wpis\u00f3w.", + "field.users.empty": "Nie wybrano jeszcze żadnych użytkowników", + + "file.delete.confirm": + "Czy na pewno chcesz usunąć
{filename}?", + + "files": "Pliki", + "files.empty": "Nie ma jeszcze żadnych plików", + + "hour": "Godzina", + "insert": "Wstaw", + "install": "Zainstaluj", + + "installation": "Instalacja", + "installation.completed": "Panel został zainstalowany", + "installation.disabled": "Instalator panelu jest domyślnie wyłączony na serwerach publicznych. Uruchom instalator na komputerze lokalnym lub włącz go za pomocą opcji panel.install.", + "installation.issues.accounts": + "Folder /site/accounts nie istnieje lub nie ma uprawnień do zapisu", + "installation.issues.content": + "Folder /content nie istnieje lub nie ma uprawnień do zapisu", + "installation.issues.curl": "Wymagane jest rozszerzenie CURL", + "installation.issues.headline": "Nie można zainstalować panelu", + "installation.issues.mbstring": + "Wymagane jest rozszerzenie MB String", + "installation.issues.media": + "Folder /media nie istnieje lub nie ma uprawnień do zapisu", + "installation.issues.php": "Upewnij się, że używasz PHP 7+", + "installation.issues.server": + "Kirby wymaga Apache, Nginx lub Caddy", + "installation.issues.sessions": "Folder /site/sessions nie istnieje lub nie ma uprawnień do zapisu", + + "language": "J\u0119zyk", + "language.code": "Kod", + "language.convert": "Ustaw jako domyślny", + "language.convert.confirm": + "

Czy na pewno chcesz zmienić domyślny język na {name}? Nie można tego cofnąć.

Jeżeli brakuje tłumaczenia jakichś treści na {name}, nie będzie ich czym zastąpić i części witryny mogą być puste.

", + "language.create": "Dodaj nowy język", + "language.delete.confirm": + "Czy na pewno chcesz usunąć język {name} i wszystkie tłumaczenia? Tego nie da się cofnąć!", + "language.deleted": "Język został usunięty", + "language.direction": "Kierunek czytania", + "language.direction.ltr": "Od lewej do prawej", + "language.direction.rtl": "Od prawej do lewej", + "language.locale": "PHP locale string", + "language.locale.warning": "You are using a custom locale set up. Please modify it in the language file in /site/languages", + "language.name": "Nazwa", + "language.updated": "Język został zaktualizowany", + + "languages": "Języki", + "languages.default": "Domyślny język", + "languages.empty": "Nie ma jeszcze żadnych języków", + "languages.secondary": "Dodatkowe języki", + "languages.secondary.empty": "Nie ma jeszcze dodatkowych języków", + + "license": "Licencja", + "license.buy": "Kup licencję", + "license.register": "Zarejestruj", + "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", + + "link": "Link", + "link.text": "Tekst linku", + + "loading": "Ładuję", + + "lock.unsaved": "Niezapisane zmiany", + "lock.unsaved.empty": "There are no more unsaved changes", + "lock.isLocked": "Niezapisane zmiany autorstwa {email}", + "lock.file.isLocked": "The file is currently being edited by {email} and cannot be changed.", + "lock.page.isLocked": "The page is currently being edited by {email} and cannot be changed.", + "lock.unlock": "Unlock", + "lock.isUnlocked": "Your unsaved changes have been overwritten by another user. You can download your changes to merge them manually.", + + "login": "Zaloguj", + "login.remember": "Nie wylogowuj mnie", + + "logout": "Wyloguj", + + "menu": "Menu", + "meridiem": "AM/PM", + "mime": "Typ multimediów", + "minutes": "Minuty", + + "month": "Miesiąc", + "months.april": "Kwiecie\u0144", + "months.august": "Sierpie\u0144", + "months.december": "Grudzie\u0144", + "months.february": "Luty", + "months.january": "Stycze\u0144", + "months.july": "Lipiec", + "months.june": "Czerwiec", + "months.march": "Marzec", + "months.may": "Maj", + "months.november": "Listopad", + "months.october": "Pa\u017adziernik", + "months.september": "Wrzesie\u0144", + + "more": "Więcej", + "name": "Nazwa", + "next": "Następne", + "off": "off", + "on": "on", + "open": "Otwórz", + "options": "Opcje", + + "orientation": "Orientacja", + "orientation.landscape": "Pozioma", + "orientation.portrait": "Pionowa", + "orientation.square": "Kwadrat", + + "page.changeSlug": "Zmie\u0144 URL", + "page.changeSlug.fromTitle": "Utw\u00f3rz na podstawie tytu\u0142u", + "page.changeStatus": "Zmień status", + "page.changeStatus.position": "Wybierz pozycję", + "page.changeStatus.select": "Wybierz nowy status", + "page.changeTemplate": "Zmień szablon", + "page.delete.confirm": + "Czy na pewno chcesz usunąć {title}?", + "page.delete.confirm.subpages": + "Ta strona zawiera podstrony.
Wszystkie podstrony również zostaną usunięte.", + "page.delete.confirm.title": "Wprowadź tytuł strony, aby potwierdzić", + "page.draft.create": "Utwórz szkic", + "page.duplicate.appendix": "Kopiuj", + "page.duplicate.files": "Copy files", + "page.duplicate.pages": "Copy pages", + "page.status": "Status", + "page.status.draft": "Szkic", + "page.status.draft.description": + "Strona jest w trybie roboczym i widoczna tylko dla zalogowanych redaktorów", + "page.status.listed": "Opublikowana", + "page.status.listed.description": "Strona jest opublikowana i widoczna dla każdego", + "page.status.unlisted": "Nie katalogowana", + "page.status.unlisted.description": "Strona jest dostępna tylko za pośrednictwem adresu URL", + + "pages": "Strony", + "pages.empty": "Nie ma jeszcze żadnych stron", + "pages.status.draft": "Szkice", + "pages.status.listed": "Opublikowane", + "pages.status.unlisted": "Nie katalogowana", + + "pagination.page": "Strona", + + "password": "Has\u0142o", + "pixel": "Piksel", + "prev": "Poprzednie", + "remove": "Usuń", + "rename": "Zmień nazwę", + "replace": "Zamie\u0144", + "retry": "Pon\u00f3w pr\u00f3b\u0119", + "revert": "Odrzu\u0107", + + "role": "Rola", + "role.admin.description": "The admin has all rights", + "role.admin.title": "Admin", + "role.all": "Wszystkie", + "role.empty": "Nie ma użytkowników z tą rolą", + "role.description.placeholder": "Brak opisu", + "role.nobody.description": "This is a fallback role without any permissions", + "role.nobody.title": "Nobody", + + "save": "Zapisz", + "search": "Szukaj", + + "section.required": "The section is required", + + "select": "Wybierz", + "settings": "Ustawienia", + "size": "Rozmiar", + "slug": "Końcówka URL", + "sort": "Sortuj", + "title": "Tytuł", + "template": "Szablon", + "today": "Dzisiaj", + + "toolbar.button.code": "Kod", + "toolbar.button.bold": "Pogrubienie", + "toolbar.button.email": "Email", + "toolbar.button.headings": "Nagłówki", + "toolbar.button.heading.1": "Nagłówek 1", + "toolbar.button.heading.2": "Nagłówek 2", + "toolbar.button.heading.3": "Nagłówek 3", + "toolbar.button.italic": "Kursywa", + "toolbar.button.file": "Plik", + "toolbar.button.file.select": "Select a file", + "toolbar.button.file.upload": "Upload a file", + "toolbar.button.link": "Link", + "toolbar.button.ol": "Lista numerowana", + "toolbar.button.ul": "Lista wypunktowana", + + "translation.author": "Zespół Kirby", + "translation.direction": "ltr", + "translation.name": "Polski", + "translation.locale": "pl_PL", + + "upload": "Prześlij", + "upload.error.cantMove": "The uploaded file could not be moved", + "upload.error.cantWrite": "Failed to write file to disk", + "upload.error.default": "The file could not be uploaded", + "upload.error.extension": "File upload stopped by extension", + "upload.error.formSize": "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the form", + "upload.error.iniPostSize": "The uploaded file exceeds the post_max_size directive in php.ini", + "upload.error.iniSize": "The uploaded file exceeds the upload_max_filesize directive in php.ini", + "upload.error.noFile": "No file was uploaded", + "upload.error.noFiles": "No files were uploaded", + "upload.error.partial": "The uploaded file was only partially uploaded", + "upload.error.tmpDir": "Missing a temporary folder", + "upload.errors": "Błąd", + "upload.progress": "Przesyłanie…", + + "url": "Url", + "url.placeholder": "https://example.com", + + "user": "Użytkownik", + "user.blueprint": + "Możesz zdefiniować dodatkowe sekcje i pola formularza dla tej roli użytkownika w /site/blueprints/users/{role}.yml", + "user.changeEmail": "Zmień email", + "user.changeLanguage": "Zmień język", + "user.changeName": "Zmień nazwę tego użytkownika", + "user.changePassword": "Zmień hasło", + "user.changePassword.new": "Nowe hasło", + "user.changePassword.new.confirm": "Potwierdź nowe hasło…", + "user.changeRole": "Zmień rolę", + "user.changeRole.select": "Wybierz nową rolę", + "user.create": "Dodaj nowego użytkownika", + "user.delete": "Usuń tego użytkownika", + "user.delete.confirm": + "Czy na pewno chcesz usunąć
{email}?", + + "users": "Użytkownicy", + + "version": "Wersja", + + "view.account": "Twoje konto", + "view.installation": "Instalacja", + "view.settings": "Ustawienia", + "view.site": "Strona", + "view.users": "U\u017cytkownicy", + + "welcome": "Witaj", + "year": "Rok" +} diff --git a/kirby/i18n/translations/pt_BR.json b/kirby/i18n/translations/pt_BR.json new file mode 100755 index 0000000..6daf754 --- /dev/null +++ b/kirby/i18n/translations/pt_BR.json @@ -0,0 +1,481 @@ +{ + "add": "Adicionar", + "avatar": "Foto do perfil", + "back": "Voltar", + "cancel": "Cancelar", + "change": "Alterar", + "close": "Fechar", + "confirm": "Salvar", + "copy": "Copiar", + "create": "Criar", + + "date": "Data", + "date.select": "Selecione uma data", + + "day": "Dia", + "days.fri": "Sex", + "days.mon": "Seg", + "days.sat": "S\u00e1b", + "days.sun": "Dom", + "days.thu": "Qui", + "days.tue": "Ter", + "days.wed": "Qua", + + "delete": "Excluir", + "dimensions": "Dimensões", + "disabled": "Disabled", + "discard": "Descartar", + "download": "Download", + "duplicate": "Duplicate", + "edit": "Editar", + + "dialog.files.empty": "No files to select", + "dialog.pages.empty": "No pages to select", + "dialog.users.empty": "No users to select", + + "email": "Email", + "email.placeholder": "mail@exemplo.com", + + "error.access.login": "Login inválido", + "error.access.panel": "Você não tem permissão para acessar o painel", + "error.access.view": "You are not allowed to access this part of the panel", + + "error.avatar.create.fail": "A foto de perfil não pôde ser enviada", + "error.avatar.delete.fail": "A foto do perfil não pôde ser deletada", + "error.avatar.dimensions.invalid": + "Por favor, use uma foto de perfil com largura e altura menores que 3000 pixels", + "error.avatar.mime.forbidden": + "A foto de perfil deve ser um arquivo JPEG ou PNG", + + "error.blueprint.notFound": "O blueprint \"{name}\" não pôde ser carregado", + + "error.email.preset.notFound": "Preset de email \"{name}\" não encontrado", + + "error.field.converter.invalid": "Conversor \"{converter}\" inválido", + + "error.file.changeName.empty": "The name must not be empty", + "error.file.changeName.permission": + "Você não tem permissão para alterar o nome de \"{filename}\"", + "error.file.duplicate": "Um arquivo com o nome \"{filename}\" já existe", + "error.file.extension.forbidden": + "Extensão \"{extension}\" não permitida", + "error.file.extension.missing": + "Extensão de \"{filename}\" em falta", + "error.file.maxheight": "The height of the image must not exceed {height} pixels", + "error.file.maxsize": "The file is too large", + "error.file.maxwidth": "The width of the image must not exceed {width} pixels", + "error.file.mime.differs": + "O arquivo enviado precisa ser do tipo \"{mime}\"", + "error.file.mime.forbidden": "Tipo de mídia \"{mime}\" não permitido", + "error.file.mime.invalid": "Invalid mime type: {mime}", + "error.file.mime.missing": + "Tipo de mídia de \"{filename}\" não detectado", + "error.file.minheight": "The height of the image must be at least {height} pixels", + "error.file.minsize": "The file is too small", + "error.file.minwidth": "The width of the image must be at least {width} pixels", + "error.file.name.missing": "O nome do arquivo não pode ficar em branco", + "error.file.notFound": "Arquivo \"{filename}\" não encontrado", + "error.file.orientation": "The orientation of the image must be \"{orientation}\"", + "error.file.type.forbidden": "Você não tem permissão para enviar arquivos {type}", + "error.file.undefined": "Arquivo n\u00e3o encontrado", + + "error.form.incomplete": "Por favor, corrija os erros do formulário…", + "error.form.notSaved": "O formulário não pôde ser salvo", + + "error.language.code": "Please enter a valid code for the language", + "error.language.duplicate": "The language already exists", + "error.language.name": "Please enter a valid name for the language", + + "error.license.format": "Please enter a valid license key", + "error.license.email": "Digite um endereço de email válido", + "error.license.verification": "The license could not be verified", + + "error.page.changeSlug.permission": + "Você não tem permissão para alterar a URL de \"{slug}\"", + "error.page.changeStatus.incomplete": + "A página possui erros e não pode ser salva", + "error.page.changeStatus.permission": + "O estado desta página não pode ser alterado", + "error.page.changeStatus.toDraft.invalid": + "A página \"{slug}\" não pode ser convertida para rascunho", + "error.page.changeTemplate.invalid": + "O tema da página \"{slug}\" não pode ser alterado", + "error.page.changeTemplate.permission": + "Você não tem permissão para alterar o tema de \"{slug}\"", + "error.page.changeTitle.empty": "O título não pode ficar em branco", + "error.page.changeTitle.permission": + "Você não tem permissão para alterar o título de \"{slug}\"", + "error.page.create.permission": "Você não tem permissão para criar \"{slug}\"", + "error.page.delete": "A página \"{slug}\" não pode ser excluída", + "error.page.delete.confirm": "Por favor, digite o título da página para confirmar", + "error.page.delete.hasChildren": + "A página possui subpáginas e não pode ser excluída", + "error.page.delete.permission": "Você não tem permissão para excluir \"{slug}\"", + "error.page.draft.duplicate": + "Um rascunho de página com a URL \"{slug}\" já existe", + "error.page.duplicate": + "Uma página com a URL \"{slug}\" já existe", + "error.page.duplicate.permission": "You are not allowed to duplicate \"{slug}\"", + "error.page.notFound": "Página\"{slug}\" não encontrada", + "error.page.num.invalid": + "Digite um número de ordenação válido. Este número não pode ser negativo.", + "error.page.slug.invalid": "Por favor, digite uma URL válida", + "error.page.sort.permission": "A página \"{slug}\" não pode ser ordenada", + "error.page.status.invalid": "Por favor, defina um estado de página válido", + "error.page.undefined": "P\u00e1gina n\u00e3o encontrada", + "error.page.update.permission": "Você não tem permissão para atualizar \"{slug}\"", + + "error.section.files.max.plural": + "Você não pode adicionar mais do que {max} arquivos à seção \"{section}\"", + "error.section.files.max.singular": + "Você não pode adicionar mais do que um arquivo à seção \"{section}\"", + "error.section.files.min.plural": + "The \"{section}\" section requires at least {min} files", + "error.section.files.min.singular": + "The \"{section}\" section requires at least one file", + + "error.section.pages.max.plural": + "Você não pode adicionar mais do que {max} página à seção \"{section}\"", + "error.section.pages.max.singular": + "Você não pode adicionar mais do que uma página à seção \"{section}\"", + "error.section.pages.min.plural": + "The \"{section}\" section requires at least {min} pages", + "error.section.pages.min.singular": + "The \"{section}\" section requires at least one page", + + "error.section.notLoaded": "A seção \"{name}\" não pôde ser carregada", + "error.section.type.invalid": "O tipo da seção \"{type}\" não é válido", + + "error.site.changeTitle.empty": "O título não pode ficar em branco", + "error.site.changeTitle.permission": + "Você não tem permissão para alterar o título do site", + "error.site.update.permission": "Você não tem permissão para atualizar o site", + + "error.template.default.notFound": "O tema padrão não existe", + + "error.user.changeEmail.permission": + "Você não tem permissão para alterar o email do usuário \"{name}\"", + "error.user.changeLanguage.permission": + "Você não tem permissão para alterar o idioma do usuário \"{name}\"", + "error.user.changeName.permission": + "Você não tem permissão para alterar o nome do usuário \"{name}\"", + "error.user.changePassword.permission": + "Você não tem permissão para alterar a senha do usuário \"{name}\"", + "error.user.changeRole.lastAdmin": + "O papel do último administrador não pode ser alterado", + "error.user.changeRole.permission": + "Você não tem permissão para alterar o papel do usuário \"{name}\"", + "error.user.changeRole.toAdmin": + "You are not allowed to promote someone to the admin role", + "error.user.create.permission": "Você não tem permissão para criar este usuário", + "error.user.delete": "O usuário \"{name}\" não pode ser excluído", + "error.user.delete.lastAdmin": "O último administrador não pode ser excluído", + "error.user.delete.lastUser": "O último usuário não pode ser excluído", + "error.user.delete.permission": + "Você não tem permissão para excluir o usuário \"{name}\"", + "error.user.duplicate": + "Um usuário com o email \"{email}\" já existe", + "error.user.email.invalid": "Digite um endereço de email válido", + "error.user.language.invalid": "Digite um idioma válido", + "error.user.notFound": "Usuário \"{name}\" não encontrado", + "error.user.password.invalid": + "Digite uma senha válida. Sua senha deve ter pelo menos 8 caracteres.", + "error.user.password.notSame": "As senhas não combinam", + "error.user.password.undefined": "O usuário não possui uma senha", + "error.user.role.invalid": "Digite um papel válido", + "error.user.update.permission": + "Você não tem permissão para atualizar o usuário \"{name}\"", + + "error.validation.accepted": "Por favor, confirme", + "error.validation.alpha": "Por favor, use apenas caracteres entre a-z", + "error.validation.alphanum": + "Por favor, use apenas caracteres entre a-z ou 0-9", + "error.validation.between": + "Digite um valor entre \"{min}\" e \"{max}\"", + "error.validation.boolean": "Por favor, confirme ou rejeite", + "error.validation.contains": + "Digite um valor que contenha \"{needle}\"", + "error.validation.date": "Escolha uma data válida", + "error.validation.date.after": "Please enter a date after {date}", + "error.validation.date.before": "Please enter a date before {date}", + "error.validation.date.between": "Please enter a date between {min} and {max}", + "error.validation.denied": "Por favor, cancele", + "error.validation.different": "O valor deve ser diferente de \"{other}\"", + "error.validation.email": "Digite um endereço de email válido", + "error.validation.endswith": "O valor deve terminar com \"{end}\"", + "error.validation.filename": "Digite um nome de arquivo válido", + "error.validation.in": "Digite um destes valores: ({in})", + "error.validation.integer": "Digite um número inteiro válido", + "error.validation.ip": "Digite um endereço de IP válido", + "error.validation.less": "Digite um valor menor que {max}", + "error.validation.match": "O valor não combina com o padrão esperado", + "error.validation.max": "Digite um valor igual ou menor que {max}", + "error.validation.maxlength": + "Digite um valor curto. (no máximo {max} caracteres)", + "error.validation.maxwords": "Digite menos que {max} palavra(s)", + "error.validation.min": "Digite um valor igual ou maior que {min}", + "error.validation.minlength": + "Digite um valor maior. (no mínimo {min} caracteres)", + "error.validation.minwords": "Digite ao menos {min} palavra(s)", + "error.validation.more": "Digite um valor maior que {min}", + "error.validation.notcontains": + "Digite um valor que não contenha \"{needle}\"", + "error.validation.notin": + "Não digite nenhum destes valores: ({notIn})", + "error.validation.option": "Escolha uma opção válida", + "error.validation.num": "Digite um número válido", + "error.validation.required": "Digite algo", + "error.validation.same": "Por favor, digite \"{other}\"", + "error.validation.size": "O tamanho do valor deve ser \"{size}\"", + "error.validation.startswith": "O valor deve começar com \"{start}\"", + "error.validation.time": "Digite uma hora válida", + "error.validation.url": "Digite uma URL válida", + + "field.required": "The field is required", + "field.files.empty": "Nenhum arquivo selecionado", + "field.pages.empty": "Nenhuma página selecionada", + "field.structure.delete.confirm": "Deseja realmente excluir este registro?", + "field.structure.empty": "Nenhum registro", + "field.users.empty": "Nenhum usuário selecionado", + + "file.delete.confirm": + "Deseja realmente excluir
{filename}?", + + "files": "Arquivos", + "files.empty": "Nenhum arquivo", + + "hour": "Hora", + "insert": "Inserir", + "install": "Instalar", + + "installation": "Instalação", + "installation.completed": "Painel instalado com sucesso", + "installation.disabled": "O instalador do painel está desabilitado em servidores públicos por padrão. Por favor, execute o instalador em uma máquina local ou habilite a opção panel.install.", + "installation.issues.accounts": + "A pasta /site/accounts não existe ou não possui permissão de escrita", + "installation.issues.content": + "A pasta /content não existe ou não possui permissão de escrita", + "installation.issues.curl": "A extensão CURL é necessária", + "installation.issues.headline": "O painel não pôde ser instalado", + "installation.issues.mbstring": + "A extensão MB String é necessária", + "installation.issues.media": + "A pasta /media não existe ou não possui permissão de escrita", + "installation.issues.php": "Certifique-se que você está usando o PHP 7+", + "installation.issues.server": + "Kirby necessita do Apache, Nginx ou Caddy", + "installation.issues.sessions": "A pasta /site/sessions não existe ou não possui permissão de escrita", + + "language": "Idioma", + "language.code": "Código", + "language.convert": "Tornar padrão", + "language.convert.confirm": + "

Deseja realmente converter {name} para o idioma padrão? Esta ação não poderá ser revertida.

Se {name} tiver conteúdo não traduzido, partes do seu site poderão ficar sem conteúdo.

", + "language.create": "Adicionar novo idioma", + "language.delete.confirm": + "Deseja realmente excluir o idioma {name} incluíndo todas as traduções. Esta ação não poderá ser revertida!", + "language.deleted": "Idioma excluído", + "language.direction": "Direção de leitura", + "language.direction.ltr": "Esquerda para direita", + "language.direction.rtl": "Direita para esquerda", + "language.locale": "String de localização do PHP", + "language.locale.warning": "You are using a custom locale set up. Please modify it in the language file in /site/languages", + "language.name": "Nome", + "language.updated": "Idioma atualizado", + + "languages": "Idiomas", + "languages.default": "Idioma padrão", + "languages.empty": "Nenhum idioma", + "languages.secondary": "Idiomas secundários", + "languages.secondary.empty": "Nenhum idioma secundário", + + "license": "Licen\u00e7a do Kirby ", + "license.buy": "Comprar licença", + "license.register": "Registrar", + "license.register.help": + "Você recebeu o código da sua licença por email após a compra. Por favor, copie e cole-a 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", + + "link": "Link", + "link.text": "Texto do link", + + "loading": "Carregando", + + "lock.unsaved": "Unsaved changes", + "lock.unsaved.empty": "There are no more unsaved changes", + "lock.isLocked": "Unsaved changes by {email}", + "lock.file.isLocked": "The file is currently being edited by {email} and cannot be changed.", + "lock.page.isLocked": "The page is currently being edited by {email} and cannot be changed.", + "lock.unlock": "Unlock", + "lock.isUnlocked": "Your unsaved changes have been overwritten by another user. You can download your changes to merge them manually.", + + "login": "Entrar", + "login.remember": "Manter-me conectado", + + "logout": "Sair", + + "menu": "Menu", + "meridiem": "AM/PM", + "mime": "Tipo de mídia", + "minutes": "Minutos", + + "month": "Mês", + "months.april": "Abril", + "months.august": "Agosto", + "months.december": "Dezembro", + "months.february": "Fevereiro", + "months.january": "Janeiro", + "months.july": "Julho", + "months.june": "Junho", + "months.march": "Mar\u00e7o", + "months.may": "Maio", + "months.november": "Novembro", + "months.october": "Outubro", + "months.september": "Setembro", + + "more": "Mais", + "name": "Nome", + "next": "Próximo", + "off": "off", + "on": "on", + "open": "Abrir", + "options": "Opções", + + "orientation": "Orientação", + "orientation.landscape": "Paisagem", + "orientation.portrait": "Retrato", + "orientation.square": "Quadrado", + + "page.changeSlug": "Alterar URL", + "page.changeSlug.fromTitle": "Criar a partir do t\u00edtulo", + "page.changeStatus": "Alterar estado", + "page.changeStatus.position": "Selecione uma posição", + "page.changeStatus.select": "Selecione um novo estado", + "page.changeTemplate": "Alterar tema", + "page.delete.confirm": + "Deseja realmente excluir {title}?", + "page.delete.confirm.subpages": + "Esta página possui subpáginas.
Todas as subpáginas serão excluídas também.", + "page.delete.confirm.title": "Digite o título da página para confirmar", + "page.draft.create": "Criar rascunho", + "page.duplicate.appendix": "Copiar", + "page.duplicate.files": "Copy files", + "page.duplicate.pages": "Copy pages", + "page.status": "Estado", + "page.status.draft": "Rascunho", + "page.status.draft.description": + "A página em modo de rascunho é visível somente para editores", + "page.status.listed": "Pública", + "page.status.listed.description": "A página pública é visível para todos", + "page.status.unlisted": "Não listadas", + "page.status.unlisted.description": "Esta página é acessível somente através da URL", + + "pages": "Páginas", + "pages.empty": "Nenhuma página", + "pages.status.draft": "Rascunhos", + "pages.status.listed": "Publicadas", + "pages.status.unlisted": "Não listadas", + + "pagination.page": "Página", + + "password": "Senha", + "pixel": "Pixel", + "prev": "Anterior", + "remove": "Remover", + "rename": "Renomear", + "replace": "Substituir", + "retry": "Tentar novamente", + "revert": "Descartar", + + "role": "Papel", + "role.admin.description": "The admin has all rights", + "role.admin.title": "Admin", + "role.all": "Todos", + "role.empty": "Não há usuários com este papel", + "role.description.placeholder": "Sem descrição", + "role.nobody.description": "This is a fallback role without any permissions", + "role.nobody.title": "Nobody", + + "save": "Salvar", + "search": "Buscar", + + "section.required": "The section is required", + + "select": "Selecionar", + "settings": "Configurações", + "size": "Tamanho", + "slug": "URL", + "sort": "Ordenar", + "title": "Título", + "template": "Tema", + "today": "Hoje", + + "toolbar.button.code": "Código", + "toolbar.button.bold": "Negrito", + "toolbar.button.email": "Email", + "toolbar.button.headings": "Títulos", + "toolbar.button.heading.1": "Título 1", + "toolbar.button.heading.2": "Título 2", + "toolbar.button.heading.3": "Título 3", + "toolbar.button.italic": "Itálico", + "toolbar.button.file": "Arquivo", + "toolbar.button.file.select": "Select a file", + "toolbar.button.file.upload": "Upload a file", + "toolbar.button.link": "Link", + "toolbar.button.ol": "Lista ordenada", + "toolbar.button.ul": "Lista não-ordenada", + + "translation.author": "Kirby Team", + "translation.direction": "ltr", + "translation.name": "Português (Brasileiro)", + "translation.locale": "pt_BR", + + "upload": "Enviar", + "upload.error.cantMove": "The uploaded file could not be moved", + "upload.error.cantWrite": "Failed to write file to disk", + "upload.error.default": "The file could not be uploaded", + "upload.error.extension": "File upload stopped by extension", + "upload.error.formSize": "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the form", + "upload.error.iniPostSize": "The uploaded file exceeds the post_max_size directive in php.ini", + "upload.error.iniSize": "The uploaded file exceeds the upload_max_filesize directive in php.ini", + "upload.error.noFile": "No file was uploaded", + "upload.error.noFiles": "No files were uploaded", + "upload.error.partial": "The uploaded file was only partially uploaded", + "upload.error.tmpDir": "Missing a temporary folder", + "upload.errors": "Erro", + "upload.progress": "Enviando…", + + "url": "Url", + "url.placeholder": "https://exemplo.com", + + "user": "Usuário", + "user.blueprint": + "Você pode definir seções e campos de formulário adicionais para este papel de usuário em /site/blueprints/users/{role}.yml", + "user.changeEmail": "Alterar email", + "user.changeLanguage": "Alterar idioma", + "user.changeName": "Renomear usuário", + "user.changePassword": "Alterar senha", + "user.changePassword.new": "Nova senha", + "user.changePassword.new.confirm": "Confirme a nova senha…", + "user.changeRole": "Alterar papel", + "user.changeRole.select": "Selecione um novo papel", + "user.create": "Adicionar novo usuário", + "user.delete": "Excluir este usuário", + "user.delete.confirm": + "Deseja realmente excluir
{email}?", + + "users": "Usuários", + + "version": "Vers\u00e3o do Kirby", + + "view.account": "Sua conta", + "view.installation": "Instala\u00e7\u00e3o", + "view.settings": "Configurações", + "view.site": "Site", + "view.users": "Usu\u00e1rios", + + "welcome": "Bem-vindo", + "year": "Ano" +} diff --git a/kirby/i18n/translations/pt_PT.json b/kirby/i18n/translations/pt_PT.json new file mode 100755 index 0000000..fb5724d --- /dev/null +++ b/kirby/i18n/translations/pt_PT.json @@ -0,0 +1,481 @@ +{ + "add": "Adicionar", + "avatar": "Foto do perfil", + "back": "Voltar", + "cancel": "Cancelar", + "change": "Alterar", + "close": "Fechar", + "confirm": "Salvar", + "copy": "Copiar", + "create": "Criar", + + "date": "Data", + "date.select": "Selecione uma data", + + "day": "Dia", + "days.fri": "Sex", + "days.mon": "Seg", + "days.sat": "S\u00e1b", + "days.sun": "Dom", + "days.thu": "Qui", + "days.tue": "Ter", + "days.wed": "Qua", + + "delete": "Excluir", + "dimensions": "Dimensões", + "disabled": "Disabled", + "discard": "Descartar", + "download": "Download", + "duplicate": "Duplicate", + "edit": "Editar", + + "dialog.files.empty": "No files to select", + "dialog.pages.empty": "No pages to select", + "dialog.users.empty": "No users to select", + + "email": "Email", + "email.placeholder": "mail@exemplo.pt", + + "error.access.login": "Login inválido", + "error.access.panel": "Não tem permissões para aceder ao painel", + "error.access.view": "You are not allowed to access this part of the panel", + + "error.avatar.create.fail": "A foto de perfil não foi enviada", + "error.avatar.delete.fail": "A foto do perfil não foi deletada", + "error.avatar.dimensions.invalid": + "Por favor, use uma foto de perfil com largura e altura menores que 3000 pixels", + "error.avatar.mime.forbidden": + "A foto de perfil deve ser um arquivo JPEG ou PNG", + + "error.blueprint.notFound": "O blueprint \"{name}\" não pode ser carregado", + + "error.email.preset.notFound": "Preset de email \"{name}\" não encontrado", + + "error.field.converter.invalid": "Conversor \"{converter}\" inválido", + + "error.file.changeName.empty": "The name must not be empty", + "error.file.changeName.permission": + "Não tem permissões para alterar o nome de \"{filename}\"", + "error.file.duplicate": "Um arquivo com o nome \"{filename}\" já existe", + "error.file.extension.forbidden": + "Extensão \"{extension}\" não permitida", + "error.file.extension.missing": + "Extensão de \"{filename}\" em falta", + "error.file.maxheight": "The height of the image must not exceed {height} pixels", + "error.file.maxsize": "The file is too large", + "error.file.maxwidth": "The width of the image must not exceed {width} pixels", + "error.file.mime.differs": + "O arquivo enviado precisa ser do tipo \"{mime}\"", + "error.file.mime.forbidden": "Tipo de mídia \"{mime}\" não permitido", + "error.file.mime.invalid": "Invalid mime type: {mime}", + "error.file.mime.missing": + "Tipo de mídia de \"{filename}\" não detectado", + "error.file.minheight": "The height of the image must be at least {height} pixels", + "error.file.minsize": "The file is too small", + "error.file.minwidth": "The width of the image must be at least {width} pixels", + "error.file.name.missing": "O nome do arquivo não pode ficar em branco", + "error.file.notFound": "Arquivo \"{filename}\" não encontrado", + "error.file.orientation": "The orientation of the image must be \"{orientation}\"", + "error.file.type.forbidden": "Não tem permissões para enviar arquivos {type}", + "error.file.undefined": "Arquivo n\u00e3o encontrado", + + "error.form.incomplete": "Por favor, corrija os erros do formulário…", + "error.form.notSaved": "O formulário não foi guardado", + + "error.language.code": "Please enter a valid code for the language", + "error.language.duplicate": "The language already exists", + "error.language.name": "Please enter a valid name for the language", + + "error.license.format": "Please enter a valid license key", + "error.license.email": "Digite um endereço de email válido", + "error.license.verification": "The license could not be verified", + + "error.page.changeSlug.permission": + "Não tem permissões para alterar a URL de \"{slug}\"", + "error.page.changeStatus.incomplete": + "A página possui erros e não pode ser guardada", + "error.page.changeStatus.permission": + "O estado desta página não pode ser alterado", + "error.page.changeStatus.toDraft.invalid": + "A página \"{slug}\" não pode ser convertida para rascunho", + "error.page.changeTemplate.invalid": + "O tema da página \"{slug}\" não pode ser alterado", + "error.page.changeTemplate.permission": + "Não tem permissões para alterar o tema de \"{slug}\"", + "error.page.changeTitle.empty": "O título não pode ficar em branco", + "error.page.changeTitle.permission": + "Não tem permissões para alterar o título de \"{slug}\"", + "error.page.create.permission": "Não tem permissões para criar \"{slug}\"", + "error.page.delete": "A página \"{slug}\" não pode ser excluída", + "error.page.delete.confirm": "Por favor, digite o título da página para confirmar", + "error.page.delete.hasChildren": + "A página possui subpáginas e não pode ser excluída", + "error.page.delete.permission": "Não tem permissões para excluir \"{slug}\"", + "error.page.draft.duplicate": + "Um rascunho de página com a URL \"{slug}\" já existe", + "error.page.duplicate": + "Uma página com a URL \"{slug}\" já existe", + "error.page.duplicate.permission": "You are not allowed to duplicate \"{slug}\"", + "error.page.notFound": "Página\"{slug}\" não encontrada", + "error.page.num.invalid": + "Digite um número de ordenação válido. Este número não pode ser negativo.", + "error.page.slug.invalid": "Por favor, digite uma URL válida", + "error.page.sort.permission": "A página \"{slug}\" não pode ser ordenada", + "error.page.status.invalid": "Por favor, defina um estado de página válido", + "error.page.undefined": "P\u00e1gina n\u00e3o encontrada", + "error.page.update.permission": "Não tem permissões para atualizar \"{slug}\"", + + "error.section.files.max.plural": + "Não pode adicionar mais do que {max} arquivos à seção \"{section}\"", + "error.section.files.max.singular": + "Não pode adicionar mais do que um arquivo à seção \"{section}\"", + "error.section.files.min.plural": + "The \"{section}\" section requires at least {min} files", + "error.section.files.min.singular": + "The \"{section}\" section requires at least one file", + + "error.section.pages.max.plural": + "Não pode adicionar mais do que {max} página à seção \"{section}\"", + "error.section.pages.max.singular": + "Não pode adicionar mais do que uma página à seção \"{section}\"", + "error.section.pages.min.plural": + "The \"{section}\" section requires at least {min} pages", + "error.section.pages.min.singular": + "The \"{section}\" section requires at least one page", + + "error.section.notLoaded": "A seção \"{name}\" não pôde ser carregada", + "error.section.type.invalid": "O tipo da seção \"{type}\" não é válido", + + "error.site.changeTitle.empty": "O título não pode ficar em branco", + "error.site.changeTitle.permission": + "Não tem permissões para alterar o título do site", + "error.site.update.permission": "Não tem permissões para atualizar o site", + + "error.template.default.notFound": "O tema padrão não existe", + + "error.user.changeEmail.permission": + "Não tem permissões para alterar o email do utilizador \"{name}\"", + "error.user.changeLanguage.permission": + "Não tem permissões para alterar o idioma do utilizador \"{name}\"", + "error.user.changeName.permission": + "Não tem permissões para alterar o nome do utilizador \"{name}\"", + "error.user.changePassword.permission": + "Não tem permissões para alterar a palavra-passe do utilizador \"{name}\"", + "error.user.changeRole.lastAdmin": + "A função do último administrador não pode ser alterado", + "error.user.changeRole.permission": + "Não tem permissões para alterar a função do utilizador \"{name}\"", + "error.user.changeRole.toAdmin": + "You are not allowed to promote someone to the admin role", + "error.user.create.permission": "Não tem permissões para criar este utilizador", + "error.user.delete": "O utilizador \"{name}\" não pode ser excluído", + "error.user.delete.lastAdmin": "O último administrador não pode ser excluído", + "error.user.delete.lastUser": "O último utilizador não pode ser excluído", + "error.user.delete.permission": + "Não tem permissões para excluir o utilizador \"{name}\"", + "error.user.duplicate": + "Um utilizador com o email \"{email}\" já existe", + "error.user.email.invalid": "Digite um endereço de email válido", + "error.user.language.invalid": "Digite um idioma válido", + "error.user.notFound": "Utilizador \"{name}\" não encontrado", + "error.user.password.invalid": + "Digite uma palavra-passe válida. A sua palavra-passe deve ter pelo menos 8 caracteres.", + "error.user.password.notSame": "As palavras-passe não combinam", + "error.user.password.undefined": "O utilizador não possui uma palavra-passe", + "error.user.role.invalid": "Digite uma função válida", + "error.user.update.permission": + "Não tem permissões para atualizar o utilizador \"{name}\"", + + "error.validation.accepted": "Por favor, confirme", + "error.validation.alpha": "Por favor, use apenas caracteres entre a-z", + "error.validation.alphanum": + "Por favor, use apenas caracteres entre a-z ou 0-9", + "error.validation.between": + "Digite um valor entre \"{min}\" e \"{max}\"", + "error.validation.boolean": "Por favor, confirme ou rejeite", + "error.validation.contains": + "Digite um valor que contenha \"{needle}\"", + "error.validation.date": "Escolha uma data válida", + "error.validation.date.after": "Please enter a date after {date}", + "error.validation.date.before": "Please enter a date before {date}", + "error.validation.date.between": "Please enter a date between {min} and {max}", + "error.validation.denied": "Por favor, cancele", + "error.validation.different": "O valor deve ser diferente de \"{other}\"", + "error.validation.email": "Digite um endereço de email válido", + "error.validation.endswith": "O valor deve terminar com \"{end}\"", + "error.validation.filename": "Digite um nome de arquivo válido", + "error.validation.in": "Digite um destes valores: ({in})", + "error.validation.integer": "Digite um número inteiro válido", + "error.validation.ip": "Digite um endereço de IP válido", + "error.validation.less": "Digite um valor menor que {max}", + "error.validation.match": "O valor não combina com o padrão esperado", + "error.validation.max": "Digite um valor igual ou menor que {max}", + "error.validation.maxlength": + "Digite um valor curto. (no máximo {max} caracteres)", + "error.validation.maxwords": "Digite menos que {max} palavra(s)", + "error.validation.min": "Digite um valor igual ou maior que {min}", + "error.validation.minlength": + "Digite um valor maior. (no mínimo {min} caracteres)", + "error.validation.minwords": "Digite ao menos {min} palavra(s)", + "error.validation.more": "Digite um valor maior que {min}", + "error.validation.notcontains": + "Digite um valor que não contenha \"{needle}\"", + "error.validation.notin": + "Não digite nenhum destes valores: ({notIn})", + "error.validation.option": "Escolha uma opção válida", + "error.validation.num": "Digite um número válido", + "error.validation.required": "Digite algo", + "error.validation.same": "Por favor, digite \"{other}\"", + "error.validation.size": "O tamanho do valor deve ser \"{size}\"", + "error.validation.startswith": "O valor deve começar com \"{start}\"", + "error.validation.time": "Digite uma hora válida", + "error.validation.url": "Digite uma URL válida", + + "field.required": "The field is required", + "field.files.empty": "Nenhum arquivo selecionado", + "field.pages.empty": "Nenhuma página selecionada", + "field.structure.delete.confirm": "Deseja realmente excluir este registro?", + "field.structure.empty": "Nenhum registro", + "field.users.empty": "Nenhum utilizador selecionado", + + "file.delete.confirm": + "Deseja realmente excluir
{filename}?", + + "files": "Arquivos", + "files.empty": "Nenhum arquivo", + + "hour": "Hora", + "insert": "Inserir", + "install": "Instalar", + + "installation": "Instalação", + "installation.completed": "Painel instalado com sucesso", + "installation.disabled": "Por padrão, o instalador do painel está desabilitado em servidores públicos. Por favor, execute o instalador numa máquina local ou habilite a opção panel.install.", + "installation.issues.accounts": + "A pasta /site/accounts não existe ou não possui permissão de escrita", + "installation.issues.content": + "A pasta /content não existe ou não possui permissão de escrita", + "installation.issues.curl": "A extensão CURL é necessária", + "installation.issues.headline": "O painel não pôde ser instalado", + "installation.issues.mbstring": + "A extensão MB String é necessária", + "installation.issues.media": + "A pasta /media não existe ou não possui permissão de escrita", + "installation.issues.php": "Certifique-se que está a usar o PHP 7+", + "installation.issues.server": + "O Kirby necessita do Apache, Nginx ou Caddy", + "installation.issues.sessions": "A pasta /site/sessions não existe ou não possui permissão de escrita", + + "language": "Idioma", + "language.code": "Código", + "language.convert": "Tornar padrão", + "language.convert.confirm": + "

Deseja realmente converter {name} para o idioma padrão? Esta ação não poderá ser revertida.

Se {name} tiver conteúdo não traduzido, partes do seu site poderão ficar sem conteúdo.

", + "language.create": "Adicionar novo idioma", + "language.delete.confirm": + "Deseja realmente excluir o idioma {name} incluíndo todas as traduções? Esta ação não poderá ser revertida!", + "language.deleted": "Idioma excluído", + "language.direction": "Direção de leitura", + "language.direction.ltr": "Esquerda para direita", + "language.direction.rtl": "Direita para esquerda", + "language.locale": "String de localização do PHP", + "language.locale.warning": "You are using a custom locale set up. Please modify it in the language file in /site/languages", + "language.name": "Nome", + "language.updated": "Idioma atualizado", + + "languages": "Idiomas", + "languages.default": "Idioma padrão", + "languages.empty": "Nenhum idioma", + "languages.secondary": "Idiomas secundários", + "languages.secondary.empty": "Nenhum idioma secundário", + + "license": "Licen\u00e7a do Kirby ", + "license.buy": "Comprar uma licença", + "license.register": "Registrar", + "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", + + "link": "Link", + "link.text": "Texto do link", + + "loading": "A carregar", + + "lock.unsaved": "Unsaved changes", + "lock.unsaved.empty": "There are no more unsaved changes", + "lock.isLocked": "Unsaved changes by {email}", + "lock.file.isLocked": "The file is currently being edited by {email} and cannot be changed.", + "lock.page.isLocked": "The page is currently being edited by {email} and cannot be changed.", + "lock.unlock": "Unlock", + "lock.isUnlocked": "Your unsaved changes have been overwritten by another user. You can download your changes to merge them manually.", + + "login": "Entrar", + "login.remember": "Manter-me conectado", + + "logout": "Sair", + + "menu": "Menu", + "meridiem": "AM/PM", + "mime": "Tipo de mídia", + "minutes": "Minutos", + + "month": "Mês", + "months.april": "Abril", + "months.august": "Agosto", + "months.december": "Dezembro", + "months.february": "Fevereiro", + "months.january": "Janeiro", + "months.july": "Julho", + "months.june": "Junho", + "months.march": "Mar\u00e7o", + "months.may": "Maio", + "months.november": "Novembro", + "months.october": "Outubro", + "months.september": "Setembro", + + "more": "Mais", + "name": "Nome", + "next": "Próximo", + "off": "off", + "on": "on", + "open": "Abrir", + "options": "Opções", + + "orientation": "Orientação", + "orientation.landscape": "Paisagem", + "orientation.portrait": "Retrato", + "orientation.square": "Quadrado", + + "page.changeSlug": "Alterar URL", + "page.changeSlug.fromTitle": "Criar a partir do t\u00edtulo", + "page.changeStatus": "Alterar estado", + "page.changeStatus.position": "Selecione uma posição", + "page.changeStatus.select": "Selecione um novo estado", + "page.changeTemplate": "Alterar tema", + "page.delete.confirm": + "Deseja realmente excluir {title}?", + "page.delete.confirm.subpages": + "Esta página possui subpáginas.
Todas as subpáginas serão excluídas também.", + "page.delete.confirm.title": "Digite o título da página para confirmar", + "page.draft.create": "Criar rascunho", + "page.duplicate.appendix": "Copiar", + "page.duplicate.files": "Copy files", + "page.duplicate.pages": "Copy pages", + "page.status": "Estado", + "page.status.draft": "Rascunho", + "page.status.draft.description": + "A página está em modo de rascunho e é visível somente para editores", + "page.status.listed": "Pública", + "page.status.listed.description": "A página é pública para todos", + "page.status.unlisted": "Não listadas", + "page.status.unlisted.description": "Esta página é acessível somente através da URL", + + "pages": "Páginas", + "pages.empty": "Nenhuma página", + "pages.status.draft": "Rascunhos", + "pages.status.listed": "Publicadas", + "pages.status.unlisted": "Não listadas", + + "pagination.page": "Página", + + "password": "Palavra-passe", + "pixel": "Pixel", + "prev": "Anterior", + "remove": "Remover", + "rename": "Renomear", + "replace": "Substituir", + "retry": "Tentar novamente", + "revert": "Descartar", + + "role": "Função", + "role.admin.description": "The admin has all rights", + "role.admin.title": "Admin", + "role.all": "Todos", + "role.empty": "Não há utilizadores com esta função", + "role.description.placeholder": "Sem descrição", + "role.nobody.description": "This is a fallback role without any permissions", + "role.nobody.title": "Nobody", + + "save": "Salvar", + "search": "Buscar", + + "section.required": "The section is required", + + "select": "Selecionar", + "settings": "Configurações", + "size": "Tamanho", + "slug": "URL", + "sort": "Ordenar", + "title": "Título", + "template": "Tema", + "today": "Hoje", + + "toolbar.button.code": "Código", + "toolbar.button.bold": "Negrito", + "toolbar.button.email": "Email", + "toolbar.button.headings": "Títulos", + "toolbar.button.heading.1": "Título 1", + "toolbar.button.heading.2": "Título 2", + "toolbar.button.heading.3": "Título 3", + "toolbar.button.italic": "Itálico", + "toolbar.button.file": "Ficheiro", + "toolbar.button.file.select": "Select a file", + "toolbar.button.file.upload": "Upload a file", + "toolbar.button.link": "Link", + "toolbar.button.ol": "Lista ordenada", + "toolbar.button.ul": "Lista não-ordenada", + + "translation.author": "Kirby Team", + "translation.direction": "ltr", + "translation.name": "Português (Europeu)", + "translation.locale": "pt_PT", + + "upload": "Enviar", + "upload.error.cantMove": "The uploaded file could not be moved", + "upload.error.cantWrite": "Failed to write file to disk", + "upload.error.default": "The file could not be uploaded", + "upload.error.extension": "File upload stopped by extension", + "upload.error.formSize": "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the form", + "upload.error.iniPostSize": "The uploaded file exceeds the post_max_size directive in php.ini", + "upload.error.iniSize": "The uploaded file exceeds the upload_max_filesize directive in php.ini", + "upload.error.noFile": "No file was uploaded", + "upload.error.noFiles": "No files were uploaded", + "upload.error.partial": "The uploaded file was only partially uploaded", + "upload.error.tmpDir": "Missing a temporary folder", + "upload.errors": "Erro", + "upload.progress": "A enviar…", + + "url": "Url", + "url.placeholder": "https://exemplo.pt", + + "user": "Utilizador", + "user.blueprint": + "Pode definir seções e campos de formulário adicionais para esta função de utilizador em /site/blueprints/users/{role}.yml", + "user.changeEmail": "Alterar email", + "user.changeLanguage": "Alterar idioma", + "user.changeName": "Renomear este utilizador", + "user.changePassword": "Alterar palavra-passe", + "user.changePassword.new": "Nova palavra-passe", + "user.changePassword.new.confirm": "Confirme a nova palavra-passe…", + "user.changeRole": "Alterar Função", + "user.changeRole.select": "Selecione uma nova função", + "user.create": "Adicionar novo utilizador", + "user.delete": "Excluir este utilizador", + "user.delete.confirm": + "Deseja realmente excluir
{email}?", + + "users": "Utilizadores", + + "version": "Vers\u00e3o do Kirby", + + "view.account": "A sua conta", + "view.installation": "Instala\u00e7\u00e3o", + "view.settings": "Configurações", + "view.site": "Site", + "view.users": "Utilizadores", + + "welcome": "Bem-vindo", + "year": "Ano" +} diff --git a/kirby/i18n/translations/ru.json b/kirby/i18n/translations/ru.json new file mode 100755 index 0000000..fb29187 --- /dev/null +++ b/kirby/i18n/translations/ru.json @@ -0,0 +1,481 @@ +{ + "add": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c", + "avatar": "\u0410\u0432\u0430\u0442\u0430\u0440 (\u0444\u043e\u0442\u043e)", + "back": "Назад", + "cancel": "\u041e\u0442\u043c\u0435\u043d\u0438\u0442\u044c", + "change": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c", + "close": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c", + "confirm": "Сохранить", + "copy": "Скопировать", + "create": "Создать", + + "date": "Дата", + "date.select": "Выберите дату", + + "day": "День", + "days.fri": "\u041f\u0442", + "days.mon": "\u041f\u043d", + "days.sat": "\u0421\u0431", + "days.sun": "\u0412\u0441", + "days.thu": "\u0427\u0442", + "days.tue": "\u0412\u0442", + "days.wed": "\u0421\u0440", + + "delete": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c", + "dimensions": "Размеры", + "disabled": "Отключено", + "discard": "\u0421\u0431\u0440\u043e\u0441", + "download": "Скачать", + "duplicate": "Дублировать", + "edit": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c", + + "dialog.files.empty": "Нет файлов для выбора", + "dialog.pages.empty": "Нет страниц для выбора", + "dialog.users.empty": "Нет пользователей для выбора", + + "email": "Эл. почта", + "email.placeholder": "pochta@domen.com", + + "error.access.login": "Неправильный логин", + "error.access.panel": "У вас нет права доступа к панели", + "error.access.view": "У вас нет прав доступа к этой части панели", + + "error.avatar.create.fail": "Не удалось загрузить фотографию профиля", + "error.avatar.delete.fail": "\u0410\u0432\u0430\u0442\u0430\u0440 (\u0444\u043e\u0442\u043e) \u043a \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0443 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0443\u0434\u0430\u043b\u0435\u043d", + "error.avatar.dimensions.invalid": + "Пожалуйста, сделайте чтобы ширина или высота фотографии была меньше 3000 пикселей", + "error.avatar.mime.forbidden": + "Фотография профиля должна быть JPEG или PNG", + + "error.blueprint.notFound": "Не удалось загрузить blueprint \"{name}\"", + + "error.email.preset.notFound": "Шаблон эл. почты \"{name}\" не найден", + + "error.field.converter.invalid": "Неверный конвертер \"{converter}\"", + + "error.file.changeName.empty": "Название не может быть пустым", + "error.file.changeName.permission": + "У вас нет права поменять название \"{filename}\"", + "error.file.duplicate": "Файл с названием \"{filename}\" уже есть", + "error.file.extension.forbidden": + "Расширение файла \"{extension}\" неразрешено", + "error.file.extension.missing": + "Файлу \"{filename}\" не хватает расширения", + "error.file.maxheight": "Высота картинки не должна превышать {height} px", + "error.file.maxsize": "Файл слишком большой", + "error.file.maxwidth": "Ширина картинки не должна превышать {width} px", + "error.file.mime.differs": + "Загруженный файл должен быть того же mime типа: \"{mime}\"", + "error.file.mime.forbidden": "Тип медиа \"{mime}\" не допустим", + "error.file.mime.invalid": "Неверный тип mime: {mime}", + "error.file.mime.missing": + "Не удалось определить тип медиа для файла \"{filename}\"", + "error.file.minheight": "Высота файла должна быть хотя бы {height} px", + "error.file.minsize": "Файл слишком маленький", + "error.file.minwidth": "Ширина файла должна быть хотя бы {width} px", + "error.file.name.missing": "Название файла не может быть пустым", + "error.file.notFound": "\u0424\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d", + "error.file.orientation": "Ориентация картинки должна быть \"{orientation}\"", + "error.file.type.forbidden": "У вас нет права загружать файлы {type}", + "error.file.undefined": "\u0424\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d", + + "error.form.incomplete": "Пожалуйста, исправьте все ошибки в форме", + "error.form.notSaved": "Форма не может быть сохранена", + + "error.language.code": "Пожалуйста, впишите правильный код языка", + "error.language.duplicate": "Язык уже есть", + "error.language.name": "Пожалуйста, впишите правильное название языка", + + "error.license.format": "Пожалуйста, введите правильный лицензионный код", + "error.license.email": "Пожалуйста, введите правильный адрес эл. почты", + "error.license.verification": "Лицензия не подтверждена", + + "error.page.changeSlug.permission": + "\u0412\u044b \u043d\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c URL \u044d\u0442\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b", + "error.page.changeStatus.incomplete": + "На странице есть ошибки и поэтому ее нельзя опубликовать", + "error.page.changeStatus.permission": + "Невозможно поменять статус для этой страницы", + "error.page.changeStatus.toDraft.invalid": + "Невозможно конвертировать в черновик страницу \"{slug}\"", + "error.page.changeTemplate.invalid": + "Невозможно поменять шаблон страницы \"{slug}\"", + "error.page.changeTemplate.permission": + "У вас нет права поменять шаблон для \"{slug}\"", + "error.page.changeTitle.empty": "Название не может быть пустым", + "error.page.changeTitle.permission": + "у вас нет права поменять название \"{slug}\"", + "error.page.create.permission": "У вас нет права создать \"{slug}\"", + "error.page.delete": "Невозможно удалить страницу \"{slug}\"", + "error.page.delete.confirm": "Впишите название страницы чтобы подтвердить", + "error.page.delete.hasChildren": + "У страницы есть внутренние страницы, поэтому ее невозможно удалить", + "error.page.delete.permission": "У вас нет права удалить \"{slug}\"", + "error.page.draft.duplicate": + "Черновик страницы с аппендиксом URL \"{slug}\" уже есть", + "error.page.duplicate": + "Страница с аппендиксом URL \"{slug}\" уже есть", + "error.page.duplicate.permission": "У вас нет права дублировать \"{slug}\"", + "error.page.notFound": "\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430", + "error.page.num.invalid": + "Пожалуйста, впишите правильное число сортировки. Число не может быть отрицательным.", + "error.page.slug.invalid": "Пожалуйста, впишите правильный префикс URL", + "error.page.sort.permission": "Невозможно сортировать страницу \"{slug}\"", + "error.page.status.invalid": "Пожалуйста, установите верный статус страницы", + "error.page.undefined": "\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430", + "error.page.update.permission": "У вас нет права обновить \"{slug}\"", + + "error.section.files.max.plural": + "Нельзя добавить больше чем {max} файлов в секции \"{section}\"", + "error.section.files.max.singular": + "Можно добавить не больше 1 файла в секции \"{section}\"", + "error.section.files.min.plural": + "Секция \"{section}\" требует хотя бы {min} файлов", + "error.section.files.min.singular": + "Секция \"{section}\" требует хотя бы 1 файл", + + "error.section.pages.max.plural": + "Можно добавить не больше {max} страниц в секции \"{section}\"", + "error.section.pages.max.singular": + "Нельзя добавить больше чем 1 страницу в секции \"{section}\"", + "error.section.pages.min.plural": + "Секция \"{section}\" требует хотя бы {min} страниц", + "error.section.pages.min.singular": + "Секция \"{section}\" требует хотя бы одну страницу", + + "error.section.notLoaded": "Секция \"{name}\" не может быть загружена", + "error.section.type.invalid": "Тип секции {type} неверный", + + "error.site.changeTitle.empty": "Название не может быть пустым", + "error.site.changeTitle.permission": + "У вас нет права поменять название сайта", + "error.site.update.permission": "У вас нет права обновить сайт", + + "error.template.default.notFound": "Нет шаблона по умолчанию", + + "error.user.changeEmail.permission": + "У вас нет права поменять эл. почту пользователя \"{name}\"", + "error.user.changeLanguage.permission": + "У вас нет права поменять язык для пользователя \"{name}\"", + "error.user.changeName.permission": + "У вас нет права поменять имя пользователя \"{name}\"", + "error.user.changePassword.permission": + "У вас нет права поменять пароль для пользователя \"{name}\"", + "error.user.changeRole.lastAdmin": + "Роль единственного администратора нельзя поменять", + "error.user.changeRole.permission": + "У вас нет права поменять поль для пользователя \"{name}\"", + "error.user.changeRole.toAdmin": + "У вас нет прав предоставить роль администратора", + "error.user.create.permission": "У вас нет права создать этого пользователя", + "error.user.delete": "\u0410\u043a\u043a\u0430\u0443\u043d\u0442 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0443\u0434\u0430\u043b\u0435\u043d", + "error.user.delete.lastAdmin": "\u0412\u044b \u043d\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0433\u043e \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430", + "error.user.delete.lastUser": "Нельзя удалить единственного пользователя", + "error.user.delete.permission": + "У вас нет права удалить пользователя \"{name}\"", + "error.user.duplicate": + "Пользователь с эл. почтой \"{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.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": "У пользователя нет пароля", + "error.user.role.invalid": "Впишите правильную роль", + "error.user.update.permission": + "У вас нет права обновить пользователя \"{name}\"", + + "error.validation.accepted": "Пожалуйста, подтвердите", + "error.validation.alpha": "Пожалуйста, впишите только буквы a-z", + "error.validation.alphanum": + "Пожалуйста, впишите только буквы a-z или числа 0-9", + "error.validation.between": + "Пожалуйста, впишите значение от \"{min}\" до \"{max}\"", + "error.validation.boolean": "Пожалуйста, подтвердите или отмените", + "error.validation.contains": + "Пожалуйста, впишите значение, которое содержит \"{needle}\"", + "error.validation.date": "Пожалуйста, впишите правильную дату", + "error.validation.date.after": "Пожалуйста, впишите дату после {date}", + "error.validation.date.before": "Пожалуйста, впишите дату до {date}", + "error.validation.date.between": "Пожалуйста, впишите дату между {min} и {max}", + "error.validation.denied": "Пожалуйста отмените", + "error.validation.different": "Значение не может быть \"{other}\"", + "error.validation.email": "Пожалуйста, введите правильный адрес эл. почты", + "error.validation.endswith": "Значение должно заканчиваться с \"{end}\"", + "error.validation.filename": "Пожалуйста, впишите правильное название файла", + "error.validation.in": "Пожалуйста, впишите одно из следующих: ({in})", + "error.validation.integer": "Пожалуйста, впишите правильное целое число", + "error.validation.ip": "Пожалуйста, впишите правильный IP адрес", + "error.validation.less": "Пожалуйста, впишите значение меньше чем {max}", + "error.validation.match": "Значение не соответствует ожидаемому шаблону", + "error.validation.max": "Пожалуйста, впишите значение равное или больше чем {max}", + "error.validation.maxlength": + "Пожалуйста, впишите значение короче (макс. {max} символов)", + "error.validation.maxwords": "Пожалуйста, впишите не более {max} слов ", + "error.validation.min": "Пожалуйста, впишите значение равное или больше чем {min}", + "error.validation.minlength": + "Пожалуйста, впишите значение длиннее (мин. {min} символов)", + "error.validation.minwords": "Пожалуйста, впишите хотя бы {min} слов", + "error.validation.more": "Пожалуйста, впишите значение больше, чем {min}", + "error.validation.notcontains": + "Пожалуйста, впишите значение, которое не содержит \"{needle}\"", + "error.validation.notin": + "Пожалуйста, не вписывайте одно из: ({notIn})", + "error.validation.option": "Пожалуйста, выберите правильную опцию ", + "error.validation.num": "Пожалуйста, впишите правильный номер", + "error.validation.required": "Пожалуйста, впишите что-нибудь", + "error.validation.same": "Пожалуйста, впишите \"{other}\"", + "error.validation.size": "Значение размера должно быть \"{size}\"", + "error.validation.startswith": "Значение должно начинаться с \"{start}\"", + "error.validation.time": "Пожалуйста, впишите правильную дату", + "error.validation.url": "Пожалуйста, впишите правильный URL", + + "field.required": "Поле обязательно", + "field.files.empty": "Еще не выбраны файлы", + "field.pages.empty": "Еще не выбраны страницы", + "field.structure.delete.confirm": "Вы точно хотите удалить эту запись?", + "field.structure.empty": "Еще нет записей", + "field.users.empty": "Еще нет пользователей", + + "file.delete.confirm": + "Вы точно хотите удалить файл?", + + "files": "Файлы", + "files.empty": "Еще нет файлов", + + "hour": "Час", + "insert": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c", + "install": "Установить", + + "installation": "Установка", + "installation.completed": "Панель установлена", + "installation.disabled": "Установка панели по умолчанию отключена на общедоступных серверах. Пожалуйста запустите установку на локальном сервере или включите такую возможность с помощью опции panel.install", + "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.php": "Убедитесь, что используется PHP 7+", + "installation.issues.server": + "Kirby требует Apache, Nginx или Caddy ", + "installation.issues.sessions": "Каталог /site/sessions не существует или нет прав записи", + + "language": "\u042f\u0437\u044b\u043a", + "language.code": "Код", + "language.convert": "Установить по умолчанию", + "language.convert.confirm": + "

Вы точно хотите конвертировать {name} в главный язык? Это нельзя будет отменить.

Если {name} имеет непереведенный контент, то больше не будет верного каскада и части вашего сайта могут быть пустыми.

", + "language.create": "Добавить новый язык", + "language.delete.confirm": + "Вы точно хотите удалить {name} язык, включая все переводы? Это нельзя будет вернуть.", + "language.deleted": "Язык удален", + "language.direction": "Направление чтения", + "language.direction.ltr": "Слева направо", + "language.direction.rtl": "Справа налево", + "language.locale": "PHP locale string", + "language.locale.warning": "Вы используете кастомную локаль. Пожалуйста измените ее в файле языка в /site/languages", + "language.name": "Название", + "language.updated": "Язык обновлен", + + "languages": "Языки", + "languages.default": "Главный язык", + "languages.empty": "Еще нет языков", + "languages.secondary": "Дополнительные языки", + "languages.secondary.empty": "Еще нет дополнительных языков", + + "license": "\u041b\u0438\u0446\u0435\u043d\u0437\u0438\u044f Kirby", + "license.buy": "Купить лицензию", + "license.register": "Зарегистрировать", + "license.register.help": + "После покупки вы получили по эл. почте код лицензии. Пожалуйста скопируйте и вставьте сюда чтобы зарегистрировать.", + "license.register.label": "Пожалуйста вставьте код лицензии", + "license.register.success": "Спасибо за поддержку Kirby", + "license.unregistered": "Это незарегистрированная версия Kirby", + + "link": "\u0421\u0441\u044b\u043b\u043a\u0430", + "link.text": "\u0422\u0435\u043a\u0441\u0442 \u0441\u0441\u044b\u043b\u043a\u0438", + + "loading": "Загрузка", + + "lock.unsaved": "Несохраненные изменения", + "lock.unsaved.empty": "Больше нет несохраненных изменений", + "lock.isLocked": "Несохраненные изменения пользователя {email}", + "lock.file.isLocked": "В данный момент этот файл редактирует {email}, поэтому его нельзя изменить.", + "lock.page.isLocked": "В данный момент эту страницу редактирует {email}, поэтому его нельзя изменить.", + "lock.unlock": "Разблокировать", + "lock.isUnlocked": "Ваши несохраненные изменения были перезаписаны другим пользователем. Вы можете загрузить ваши изменения и объединить их вручную.", + + "login": "Войти", + "login.remember": "Сохранять вход активным", + + "logout": "Выйти", + + "menu": "Меню", + "meridiem": "До полудня / После полудня", + "mime": "Тип медиа", + "minutes": "Минуты", + + "month": "Месяц", + "months.april": "\u0410\u043f\u0440\u0435\u043b\u044c", + "months.august": "\u0410\u0432\u0433\u0443\u0441\u0442", + "months.december": "\u0414\u0435\u043a\u0430\u0431\u0440\u044c", + "months.february": "\u0424\u0435\u0432\u0440\u0430\u043b\u044c", + "months.january": "\u042f\u043d\u0432\u0430\u0440\u044c", + "months.july": "\u0418\u044e\u043b\u044c", + "months.june": "\u0418\u044e\u043d\u044c", + "months.march": "\u041c\u0430\u0440\u0442", + "months.may": "\u041c\u0430\u0439", + "months.november": "\u041d\u043e\u044f\u0431\u0440\u044c", + "months.october": "\u041e\u043a\u0442\u044f\u0431\u0440\u044c", + "months.september": "\u0421\u0435\u043d\u0442\u044f\u0431\u0440\u044c", + + "more": "Подробнее", + "name": "Название", + "next": "Дальше", + "off": "выключено", + "on": "включено", + "open": "Открыть", + "options": "Опции", + + "orientation": "Ориентация", + "orientation.landscape": "Горизонтальная", + "orientation.portrait": "Портретная", + "orientation.square": "Квадрат", + + "page.changeSlug": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u0441\u0441\u044b\u043b\u043a\u0443 (\u0427\u041f\u0423)", + "page.changeSlug.fromTitle": "Создать из названия", + "page.changeStatus": "Изменить статус", + "page.changeStatus.position": "Пожалуйста, выберите позицию", + "page.changeStatus.select": "Выбрать новый статус", + "page.changeTemplate": "Поменять шаблон", + "page.delete.confirm": + "Вы точно хотите удалить эту страницу?", + "page.delete.confirm.subpages": + "У этой страницы есть внутренние страницы.
Все внутренние страницы так же будут удалены.", + "page.delete.confirm.title": "Напишите название страницы, чтобы подтвердить", + "page.draft.create": "Создать черновик", + "page.duplicate.appendix": "Скопировать", + "page.duplicate.files": "Копировать файлы", + "page.duplicate.pages": "Копировать страницы", + "page.status": "Статус", + "page.status.draft": "Черновик", + "page.status.draft.description": + "Страница в режиме черновика и видна только для авторизированных редакторов", + "page.status.listed": "Опубликована", + "page.status.listed.description": "Страница доступна для всех посетителей", + "page.status.unlisted": "Скрыта", + "page.status.unlisted.description": "Страница доступна только по URL", + + "pages": "Страницы", + "pages.empty": "Еще нет страниц", + "pages.status.draft": "Черновики", + "pages.status.listed": "Опубликовано", + "pages.status.unlisted": "Скрытая", + + "pagination.page": "Страница", + + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "pixel": "Пиксель", + "prev": "Предыдущий", + "remove": "Удалить", + "rename": "Переназвать", + "replace": "\u0417\u0430\u043c\u0435\u043d\u0438\u0442\u044c", + "retry": "\u041f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u044c", + "revert": "\u0421\u0431\u0440\u043e\u0441", + + "role": "\u0420\u043e\u043b\u044c", + "role.admin.description": "Admin имеет все права", + "role.admin.title": "Admin", + "role.all": "Все", + "role.empty": "Нет пользователей с такой ролью", + "role.description.placeholder": "Без описания", + "role.nobody.description": "Эта роль применяется если у пользователя нет никаких прав", + "role.nobody.title": "Никто", + + "save": "\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c", + "search": "Поиск", + + "section.required": "Секция обязательна", + + "select": "Выбрать", + "settings": "Настройка", + "size": "Размер", + "slug": "Понятная ссылка (ЧПУ)", + "sort": "Сортировать", + "title": "Название", + "template": "\u0428\u0430\u0431\u043b\u043e\u043d", + "today": "Сегодня", + + "toolbar.button.code": "Код", + "toolbar.button.bold": "\u0416\u0438\u0440\u043d\u044b\u0439 \u0448\u0440\u0438\u0444\u0442", + "toolbar.button.email": "\u042d\u043b.\u043f\u043e\u0447\u0442\u0430", + "toolbar.button.headings": "Заголовки", + "toolbar.button.heading.1": "Заголовок 1", + "toolbar.button.heading.2": "Заголовок 2", + "toolbar.button.heading.3": "Заголовок 3", + "toolbar.button.italic": "\u041d\u0430\u043a\u043b\u043e\u043d\u043d\u044b\u0439 \u0448\u0440\u0438\u0444\u0442", + "toolbar.button.file": "Файл", + "toolbar.button.file.select": "Выбрать файл", + "toolbar.button.file.upload": "Закачать файл", + "toolbar.button.link": "\u0421\u0441\u044b\u043b\u043a\u0430", + "toolbar.button.ol": "Нумерованный список", + "toolbar.button.ul": "Маркированный список", + + "translation.author": "Команда Кирби", + "translation.direction": "ltr", + "translation.name": "Русский (Russian)", + "translation.locale": "ru_RU", + + "upload": "Закачать", + "upload.error.cantMove": "Загруженный файл не может быть перемещен", + "upload.error.cantWrite": "Не получилось записать файл на диск", + "upload.error.default": "Не получилось загрузить файл", + "upload.error.extension": "Загрузка файла не удалась из за расширения", + "upload.error.formSize": "Загруженный файл больше чем MAX_FILE_SIZE настройка в форме", + "upload.error.iniPostSize": "Загружаемый файл больше чем post_max_size настройка в php.ini", + "upload.error.iniSize": "Загруженный файл больше чем upload_max_filesize настройка в php.ini", + "upload.error.noFile": "Файл не был загружен", + "upload.error.noFiles": "Файлы не были загружены", + "upload.error.partial": "Файл загружен только частично", + "upload.error.tmpDir": "Не хватает временной папки", + "upload.errors": "Ошибка", + "upload.progress": "Закачивается...", + + "url": "Url", + "url.placeholder": "https://example.com", + + "user": "Пользователь", + "user.blueprint": + "Вы можете определить новые секции и поля формы для этой роли пользователя в /site/blueprints/users/{role}.yml", + "user.changeEmail": "Поменять эл. почту", + "user.changeLanguage": "Поменять язык", + "user.changeName": "Переназвать этого пользователя", + "user.changePassword": "Поменять пароль", + "user.changePassword.new": "Новый пароль", + "user.changePassword.new.confirm": "Подтвердить новый пароль", + "user.changeRole": "Поменять роль", + "user.changeRole.select": "Выбрать новую роль", + "user.create": "Добавить нового пользователя", + "user.delete": "Удалить этого пользователя", + "user.delete.confirm": + "\u0412\u044b \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0430\u043a\u043a\u0430\u0443\u043d\u0442?", + + "users": "Пользователи", + + "version": "\u0412\u0435\u0440\u0441\u0438\u044f Kirby", + + "view.account": "\u0412\u0430\u0448 \u0430\u043a\u043a\u0430\u0443\u043d\u0442", + "view.installation": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430", + "view.settings": "Настройка", + "view.site": "Сайт", + "view.users": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438", + + "welcome": "Добро пожаловать", + "year": "Год" +} diff --git a/kirby/i18n/translations/sk.json b/kirby/i18n/translations/sk.json new file mode 100755 index 0000000..52d8fec --- /dev/null +++ b/kirby/i18n/translations/sk.json @@ -0,0 +1,481 @@ +{ + "add": "Pridať", + "avatar": "Profilový obrázok", + "back": "Späť", + "cancel": "Zrušiť", + "change": "Zmeniť", + "close": "Zavrieť", + "confirm": "Ok", + "copy": "Kopírovať", + "create": "Vytvoriť", + + "date": "Dátum", + "date.select": "Zvoliť dátum", + + "day": "Deň", + "days.fri": "Pia", + "days.mon": "Pon", + "days.sat": "Sob", + "days.sun": "Ned", + "days.thu": "Štv", + "days.tue": "Uto", + "days.wed": "Str", + + "delete": "Zmazať", + "dimensions": "Rozmery", + "disabled": "Disabled", + "discard": "Zahodiť", + "download": "Download", + "duplicate": "Duplicate", + "edit": "Upraviť", + + "dialog.files.empty": "No files to select", + "dialog.pages.empty": "No pages to select", + "dialog.users.empty": "No users to select", + + "email": "E-mail", + "email.placeholder": "mail@example.com", + + "error.access.login": "Neplatné prihlásenie", + "error.access.panel": "Nemáte povolenie na prístup do Panel-u", + "error.access.view": "You are not allowed to access this part of the panel", + + "error.avatar.create.fail": "Profilový obrázok sa nepodarilo nahrať", + "error.avatar.delete.fail": "Profilový obrázok sa nepodarilo zmazať", + "error.avatar.dimensions.invalid": + "Prosím, dodržte, aby šírka a výška profilového obrázka bola menšia ako 3000 pixelov.", + "error.avatar.mime.forbidden": + "Profilový obrázok musí byť súbor JPEG alebo PNG.", + + "error.blueprint.notFound": "Blueprint \"{name}\" sa nepodarilo načítať", + + "error.email.preset.notFound": "E-mailovú predvoľbu \"{name}\" nie je možné nájsť", + + "error.field.converter.invalid": "Neplatný converter \"{converter}\"", + + "error.file.changeName.empty": "The name must not be empty", + "error.file.changeName.permission": + "Nemáte povolenie na zmenu názvu pre \"{filename}\"", + "error.file.duplicate": "Súbor s názvom \"{filename}\" už existuje", + "error.file.extension.forbidden": + "Prípona \"{extension}\" nie je povolená", + "error.file.extension.missing": + "Prípona pre \"{filename}\" chýba", + "error.file.maxheight": "The height of the image must not exceed {height} pixels", + "error.file.maxsize": "The file is too large", + "error.file.maxwidth": "The width of the image must not exceed {width} pixels", + "error.file.mime.differs": + "MIME typ nahratého súboru msa musí zhodovať s \"{mime}\"", + "error.file.mime.forbidden": "Typ média \"{mime}\" nie je povolený", + "error.file.mime.invalid": "Invalid mime type: {mime}", + "error.file.mime.missing": + "Typ média pre \"{filename}\" sa nepodarilo zistiť", + "error.file.minheight": "The height of the image must be at least {height} pixels", + "error.file.minsize": "The file is too small", + "error.file.minwidth": "The width of the image must be at least {width} pixels", + "error.file.name.missing": "Názov súboru nemôže byť prázdny", + "error.file.notFound": "Súbor \"{filename}\" sa nepodarilo nájsť", + "error.file.orientation": "The orientation of the image must be \"{orientation}\"", + "error.file.type.forbidden": "Nemáte povolenie na nahrávanie súborov s typom {type}", + "error.file.undefined": "Súbor nie je možné nájsť", + + "error.form.incomplete": "Prosím, opravte všetky chyby v rámci formuláru...", + "error.form.notSaved": "Formulár sa nepodarilo uložiť", + + "error.language.code": "Please enter a valid code for the language", + "error.language.duplicate": "The language already exists", + "error.language.name": "Please enter a valid name for the language", + + "error.license.format": "Please enter a valid license key", + "error.license.email": "Prosím, zadajte platnú e-mailovú adresu", + "error.license.verification": "The license could not be verified", + + "error.page.changeSlug.permission": + "Nemáte povolenie na zmenu URL príponu pre \"{slug}\"", + "error.page.changeStatus.incomplete": + "Stránka obsahuje chyby a nemôže byť zverejnená", + "error.page.changeStatus.permission": + "Status tejto stránky nemôže byť zmenený", + "error.page.changeStatus.toDraft.invalid": + "Stránka \"{slug}\" nemôže byť zmenená na koncept.", + "error.page.changeTemplate.invalid": + "Šablónu pre stránku \"{slug}\" nie je možné zmeniť", + "error.page.changeTemplate.permission": + "Nemáte povolenie na zmenu šablóny pre \"{slug}\"", + "error.page.changeTitle.empty": "Titulok nemôže byť prázdny", + "error.page.changeTitle.permission": + "Nemáte povolenie na zmenu titulku pre \"{slug}\"", + "error.page.create.permission": "Nemáte povolenie na vytvorenie \"{slug}\"", + "error.page.delete": "Stránku \"{slug}\" nie je možné vymazať", + "error.page.delete.confirm": "Prosím, zadajte titulok stránky pre potvrdenie", + "error.page.delete.hasChildren": + "Táto stránka obsahuje podstránky a nemôže byť zmazaná", + "error.page.delete.permission": "Nemáte povolenie na zmazanie stránky \"{slug}\"", + "error.page.draft.duplicate": + "Koncept stránky s URL appendix-om \"{slug}\" už existuje", + "error.page.duplicate": + "Stránka s URL appendix-om \"{slug}\" už existuje", + "error.page.duplicate.permission": "You are not allowed to duplicate \"{slug}\"", + "error.page.notFound": "Stránku \"{slug}\" nie je možné nájsť", + "error.page.num.invalid": + "Prosím, zadajte platné číslo pre radenie. Čísla nemôžu byť záporné.", + "error.page.slug.invalid": "Prosím, zadajte platný URL prefix.", + "error.page.sort.permission": "Stránku \"{slug}\" nie je možné preradiť.", + "error.page.status.invalid": "Prosím, nastavte platnú status pre stránku", + "error.page.undefined": "Stránku nie je možné nájsť", + "error.page.update.permission": "Nemáte povolenie na aktualizáciu \"{slug}\"", + + "error.section.files.max.plural": + "Nemôžete pridať viac ako {max} súbory/ov do sekcie \"{section}\"", + "error.section.files.max.singular": + "Nemôžete pridať viac ako 1 súbor do sekcie \"{section}\"", + "error.section.files.min.plural": + "The \"{section}\" section requires at least {min} files", + "error.section.files.min.singular": + "The \"{section}\" section requires at least one file", + + "error.section.pages.max.plural": + "Nemôžete pridať viac ako {max} stránky/ok do sekcie \"{section}\"", + "error.section.pages.max.singular": + "Nemôžete pridať viac ako 1 stránku do sekcie \"{section}\"", + "error.section.pages.min.plural": + "The \"{section}\" section requires at least {min} pages", + "error.section.pages.min.singular": + "The \"{section}\" section requires at least one page", + + "error.section.notLoaded": "Sekciu \"{name}\" sa nepodarilo nahrať", + "error.section.type.invalid": "Typ sekcie \"{type}\" nie je platný", + + "error.site.changeTitle.empty": "Titulok nemôže byť prázdny", + "error.site.changeTitle.permission": + "Nemáte povolenie na zmenu titulku pre portál", + "error.site.update.permission": "Nemáte povolenie na aktualizovanie portálu", + + "error.template.default.notFound": "Predvolená šablóna neexistuje", + + "error.user.changeEmail.permission": + "Nemáte povolenie na zmenu e-mailu pre užívateľa \"{name}\"", + "error.user.changeLanguage.permission": + "Nemáte povolenie na zmenu jazyka pre užívateľa \"{name}\"", + "error.user.changeName.permission": + "Nemáte povolenie na zmenu mena pre užívateľa \"{name}\"", + "error.user.changePassword.permission": + "Nemáte povolenie na zmenu hesla pre užívateľa \"{name}\"", + "error.user.changeRole.lastAdmin": + "Rolu pre posledného administrátora nie je možné zmeniť", + "error.user.changeRole.permission": + "Nemáte povolenie na zmenu role pre užívateľa \"{name}\"", + "error.user.changeRole.toAdmin": + "You are not allowed to promote someone to the admin role", + "error.user.create.permission": "Nemáte povolenie na vytvorenie tohto užívateľa", + "error.user.delete": "Užívateľa \"{name}\" nie je možné zmazať", + "error.user.delete.lastAdmin": "Posledného administrátora nie je možné zmazať", + "error.user.delete.lastUser": "Posledného užívateľa nie je možné zmazať", + "error.user.delete.permission": + "Nemáte povolenie na zmazanie užívateľa \"{name}\"", + "error.user.duplicate": + "Užívateľ s e-mailovou adresou \"{email}\" už existuje", + "error.user.email.invalid": "Prosím, zadajte platnú e-mailovú adresu", + "error.user.language.invalid": "Prosím, zadajte platný jazyk", + "error.user.notFound": "Užívateľa \"{name}\" nie je možné nájsť", + "error.user.password.invalid": + "Prosím, zadajte platné heslo. Dĺžka hesla musí byť aspoň 8 znakov.", + "error.user.password.notSame": "Heslá nie sú rovnaké", + "error.user.password.undefined": "Užívateľ nemá heslo", + "error.user.role.invalid": "Prosím, zadajte platnú rolu", + "error.user.update.permission": + "Nemáte povolenie na aktualizáciu užívateľa \"{name}\"", + + "error.validation.accepted": "Prosím, potvrďte", + "error.validation.alpha": "Prosím, zadajte len znaky z hlások a-z", + "error.validation.alphanum": + "Prosím, zadajte len znaky z hlások a-z a čísloviek 0-9", + "error.validation.between": + "Prosím, zadajte hodnotu od \"{min}\" do \"{max}\"", + "error.validation.boolean": "Prosím, potvrďte alebo odmietnite", + "error.validation.contains": + "Prosím, zadajte hodnotu, ktorá obsahuje \"{needle}\"", + "error.validation.date": "Prosím, zadajte platný dátum", + "error.validation.date.after": "Please enter a date after {date}", + "error.validation.date.before": "Please enter a date before {date}", + "error.validation.date.between": "Please enter a date between {min} and {max}", + "error.validation.denied": "Prosím, odmietnite", + "error.validation.different": "Hodnota nemôže byť \"{other}\"", + "error.validation.email": "Prosím, zadajte platnú e-mailovú adresu", + "error.validation.endswith": "Hodnota musí končiť na \"{end}\"", + "error.validation.filename": "Prosím, zadajte platný názov súboru", + "error.validation.in": "Prosím, zadajte jedno z nasledujúcich: ({in})", + "error.validation.integer": "Prosím, zadajte platné celé číslo", + "error.validation.ip": "Prosím, zadajte platnú e-mailovú adresu", + "error.validation.less": "Prosím, zadajte hodnotu menšiu ako {max}", + "error.validation.match": "Hodnota nezodpovedá očakávanému vzoru", + "error.validation.max": "Prosím, zadajte hodnotu rovnú alebo menšiu ako {max}", + "error.validation.maxlength": + "Prosím, zadajte kratšiu hodnotu. (max. {max} charaktery/ov)", + "error.validation.maxwords": "Prosím, nezadávajte viac ako {max} slovo/á/ov", + "error.validation.min": "Prosím, zadajte hodnotu rovnú alebo väčšiu ako {min}", + "error.validation.minlength": + "Prosím, zadajte dlhšiu hodnotu. (min. {min} charaktery/ov)", + "error.validation.minwords": "Prosím, zadajte aspoň {min} slovo/á/ov", + "error.validation.more": "Prosím zadajte hodnotu väčšiu ako {min}", + "error.validation.notcontains": + "Prosím, zadajte hodnotu, ktorá neobsahuje \"{needle}\"", + "error.validation.notin": + "Prosím, nezadávajte ani jedno z nasledujúcich: ({notIn})", + "error.validation.option": "Prosím, zadajte platnú voľbu", + "error.validation.num": "Prosím, zadajte platné číslo", + "error.validation.required": "Prosím, zadajte niečo", + "error.validation.same": "Prosím, zadajte \"{other}\"", + "error.validation.size": "Veľkosť hodnoty musí byť \"{size}\"", + "error.validation.startswith": "Hodnota musí začínať s \"{start}\"", + "error.validation.time": "Prosím, zadajte platný čas", + "error.validation.url": "Prosím, zadajte platnú URL", + + "field.required": "The field is required", + "field.files.empty": "Žiadne súbory zatiaľ neboli zvolené", + "field.pages.empty": "Žiadne stránky zatiaľ neboli zvolené", + "field.structure.delete.confirm": "Ste si istý, že chcete zmazať tento riadok?", + "field.structure.empty": "Zatiaľ žiadne údaje", + "field.users.empty": "Žiadni užívatelia zatiaľ neboli zvolení", + + "file.delete.confirm": + "Ste si istý, že chcete zmazať
{filename}?", + + "files": "Súbory", + "files.empty": "Zatiaľ žiadne súbory", + + "hour": "Hodina", + "insert": "Vložiť", + "install": "Inštalovať", + + "installation": "Inštalácia", + "installation.completed": "Panel bol nainštalovaný", + "installation.disabled": "Inštalácia Panelu na verejných serveroch je štandardne zablokovaná. Prosím, spustite inštaláciu na lokálnom serveri alebo aktivujte voľbu panel.install.", + "installation.issues.accounts": + "Priečinok /site/accounts neexistuje alebo nie je nastavený ako zapisovateľný", + "installation.issues.content": + "Priečinok /content neexistuje alebo nie je nastavený ako zapisovateľný", + "installation.issues.curl": "CURL rozšírenie je povinné", + "installation.issues.headline": "Panel nie je možné naištalovať", + "installation.issues.mbstring": + "MB String rozšírenie je povinné", + "installation.issues.media": + "Priečinok /media neexistuje alebo nie je nastavený ako zapisovateľný", + "installation.issues.php": "Uistite sa, že používate PHP 7+", + "installation.issues.server": + "Kirby vyžaduje Apache, Nginx alebo Caddy", + "installation.issues.sessions": "Priečinok /site/sessions neexistuje alebo nie je nastavený ako zapisovateľný", + + "language": "Jazyk", + "language.code": "Kód", + "language.convert": "Nastaviť ako predvolené", + "language.convert.confirm": + "

Ste si istý, že chcete nastaviť {name} ako predvolený jazyk? Túto akciu nie je možné zvrátiť.

Ak {name} obsahuje nepreložený obsah, tak pre tento obsah nebude fungovať platné volanie a niektoré časti vašich stránok zostanú prázdne.

", + "language.create": "Pridať nový jazyk", + "language.delete.confirm": + "Ste si istý, že chcete zmazať jazyk {name} vrátane všetkých prekladov? Túto akciu nie je možné zvrátiť.", + "language.deleted": "Jazyk bol zmazaný", + "language.direction": "Smer čítania", + "language.direction.ltr": "Zľava doprava", + "language.direction.rtl": "Zprava doľava", + "language.locale": "PHP locale string", + "language.locale.warning": "You are using a custom locale set up. Please modify it in the language file in /site/languages", + "language.name": "Názov", + "language.updated": "Jazyk bol aktualizovaný", + + "languages": "Jazyky", + "languages.default": "Predvolený jazyk", + "languages.empty": "Zatiaľ žiadne jazyky", + "languages.secondary": "Sekundárne jazyky", + "languages.secondary.empty": "Zatiaľ žiadne sekundárne jazyky", + + "license": "Licencia", + "license.buy": "Zakúpiť licenciu", + "license.register": "Registrovať", + "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", + + "link": "Odkaz", + "link.text": "Text odkazu", + + "loading": "Načítavanie", + + "lock.unsaved": "Unsaved changes", + "lock.unsaved.empty": "There are no more unsaved changes", + "lock.isLocked": "Unsaved changes by {email}", + "lock.file.isLocked": "The file is currently being edited by {email} and cannot be changed.", + "lock.page.isLocked": "The page is currently being edited by {email} and cannot be changed.", + "lock.unlock": "Unlock", + "lock.isUnlocked": "Your unsaved changes have been overwritten by another user. You can download your changes to merge them manually.", + + "login": "Prihlásenie", + "login.remember": "Ponechať ma prihláseného", + + "logout": "Odhlásenie", + + "menu": "Menu", + "meridiem": "AM/PM", + "mime": "Typ média", + "minutes": "Minúty", + + "month": "Mesiac", + "months.april": "Apríl", + "months.august": "August", + "months.december": "December", + "months.february": "Február", + "months.january": "Január", + "months.july": "Júl", + "months.june": "Jún", + "months.march": "Marec", + "months.may": "Máj", + "months.november": "November", + "months.october": "Október", + "months.september": "September", + + "more": "Viac", + "name": "Meno", + "next": "Ďalej", + "off": "off", + "on": "on", + "open": "Otvoriť", + "options": "Nastavenia", + + "orientation": "Orientácia", + "orientation.landscape": "Širokouhlá", + "orientation.portrait": "Portrét", + "orientation.square": "Štvorec", + + "page.changeSlug": "Zmeniť URL", + "page.changeSlug.fromTitle": "Vytvoriť z titulku", + "page.changeStatus": "Zmeniť status", + "page.changeStatus.position": "Prosím, zmeňte pozíciu", + "page.changeStatus.select": "Zvoľte nový status", + "page.changeTemplate": "Zmeniť šablónu", + "page.delete.confirm": + "Ste si istý, že chcete zmazať {title}?", + "page.delete.confirm.subpages": + "Táto stránka obsahuje podstránky.
Všetky podstránky budú taktiež zmazané.", + "page.delete.confirm.title": "Pre potvrdenie zadajte titulok stránky", + "page.draft.create": "Vytvoriť koncept", + "page.duplicate.appendix": "Kopírovať", + "page.duplicate.files": "Copy files", + "page.duplicate.pages": "Copy pages", + "page.status": "Status", + "page.status.draft": "Koncept", + "page.status.draft.description": + "Stránka je koncept móde a je viditeľná len pre prihlásených užívateľov", + "page.status.listed": "Verejné", + "page.status.listed.description": "Stránka je prístupná pre všetkých", + "page.status.unlisted": "Skryté", + "page.status.unlisted.description": "Stránka je prístupná len prostredníctvom priamej URL", + + "pages": "Stránky", + "pages.empty": "Zatiaľ žiadne stránky", + "pages.status.draft": "Koncepty", + "pages.status.listed": "Zverejnené", + "pages.status.unlisted": "Skryté", + + "pagination.page": "Stránka", + + "password": "Heslo", + "pixel": "Pixel", + "prev": "Predchádzajúci", + "remove": "Odstrániť", + "rename": "Premenovať", + "replace": "Nahradiť", + "retry": "Skúsiť ešte raz", + "revert": "Vrátiť späť", + + "role": "Rola", + "role.admin.description": "The admin has all rights", + "role.admin.title": "Admin", + "role.all": "Všetko", + "role.empty": "S touto rolou neexistujú žiadni užívatelia", + "role.description.placeholder": "Žiadny popis", + "role.nobody.description": "This is a fallback role without any permissions", + "role.nobody.title": "Nobody", + + "save": "Uložiť", + "search": "Hľadať", + + "section.required": "The section is required", + + "select": "Zvoliť", + "settings": "Nastavenia", + "size": "Veľkosť", + "slug": "URL appendix", + "sort": "Zoradiť", + "title": "Titulok", + "template": "Šablóna", + "today": "Dnes", + + "toolbar.button.code": "Kód", + "toolbar.button.bold": "Tučný", + "toolbar.button.email": "E-mail", + "toolbar.button.headings": "Nadpisy", + "toolbar.button.heading.1": "Nadpis 1", + "toolbar.button.heading.2": "Nadpis 2", + "toolbar.button.heading.3": "Nadpis 3", + "toolbar.button.italic": "Kurzíva", + "toolbar.button.file": "Súbor", + "toolbar.button.file.select": "Select a file", + "toolbar.button.file.upload": "Upload a file", + "toolbar.button.link": "Odkaz", + "toolbar.button.ol": "Číslovaný zoznam", + "toolbar.button.ul": "Odrážkový zoznam", + + "translation.author": "Tím Kirby", + "translation.direction": "ltr", + "translation.name": "Slovensky", + "translation.locale": "sk_SK", + + "upload": "Nahrať", + "upload.error.cantMove": "The uploaded file could not be moved", + "upload.error.cantWrite": "Failed to write file to disk", + "upload.error.default": "The file could not be uploaded", + "upload.error.extension": "File upload stopped by extension", + "upload.error.formSize": "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the form", + "upload.error.iniPostSize": "The uploaded file exceeds the post_max_size directive in php.ini", + "upload.error.iniSize": "The uploaded file exceeds the upload_max_filesize directive in php.ini", + "upload.error.noFile": "No file was uploaded", + "upload.error.noFiles": "No files were uploaded", + "upload.error.partial": "The uploaded file was only partially uploaded", + "upload.error.tmpDir": "Missing a temporary folder", + "upload.errors": "Chyba", + "upload.progress": "Nahrávanie...", + + "url": "URL", + "url.placeholder": "https://example.com", + + "user": "Užívateľ", + "user.blueprint": + "Ďalšie sekcie a formulárové polia pre túto užívateľskú rolu môžete definovať v rámci /site/blueprints/users/{role}.yml", + "user.changeEmail": "Zmeniť e-mail", + "user.changeLanguage": "Zmeniť jazyk", + "user.changeName": "Premenovať tohto užívateľa", + "user.changePassword": "Zmeniť heslo", + "user.changePassword.new": "Nové heslo", + "user.changePassword.new.confirm": "Potvrdiť nové heslo...", + "user.changeRole": "Zmeniť rolu", + "user.changeRole.select": "Zvoliť novú rolu", + "user.create": "Pridať nového užívateľa", + "user.delete": "Zmazať tohto užívateľa", + "user.delete.confirm": + "Ste si istý, že chcete zmazať
{email}?", + + "users": "Užívatelia", + + "version": "Verzia", + + "view.account": "Váš účet", + "view.installation": "Inštalácia", + "view.settings": "Nastavenia", + "view.site": "Portál", + "view.users": "Užívatelia", + + "welcome": "Vitajte", + "year": "Rok" +} diff --git a/kirby/i18n/translations/sv_SE.json b/kirby/i18n/translations/sv_SE.json new file mode 100755 index 0000000..db48fd0 --- /dev/null +++ b/kirby/i18n/translations/sv_SE.json @@ -0,0 +1,481 @@ +{ + "add": "L\u00e4gg till", + "avatar": "Profilbild", + "back": "Tillbaka", + "cancel": "Avbryt", + "change": "\u00c4ndra", + "close": "St\u00e4ng", + "confirm": "Spara", + "copy": "Kopiera", + "create": "Skapa", + + "date": "Datum", + "date.select": "Välj ett datum", + + "day": "Dag", + "days.fri": "Fre", + "days.mon": "M\u00e5n", + "days.sat": "L\u00f6r", + "days.sun": "S\u00f6n", + "days.thu": "Tor", + "days.tue": "Tis", + "days.wed": "Ons", + + "delete": "Radera", + "dimensions": "Dimensioner", + "disabled": "Inaktiverad", + "discard": "Kassera", + "download": "Ladda ner", + "duplicate": "Duplicera", + "edit": "Redigera", + + "dialog.files.empty": "Inga filer att välja", + "dialog.pages.empty": "Inga sidor att välja", + "dialog.users.empty": "Inga användare att välja", + + "email": "E-post", + "email.placeholder": "namn@exempel.se", + + "error.access.login": "Ogiltig inloggning", + "error.access.panel": "Du saknar behörighet att nå panelen", + "error.access.view": "Du saknar behörighet att nå denna del av panelen", + + "error.avatar.create.fail": "Profilbilden kunde inte laddas upp", + "error.avatar.delete.fail": "Profilbilden kunde inte raderas", + "error.avatar.dimensions.invalid": + "Se till att profilbildens bredd och höjd är mindre än 3000 pixlar", + "error.avatar.mime.forbidden": + "Profilbilden måste vara i formatet JPEG eller PNG", + + "error.blueprint.notFound": "Blueprint \"{name}\" kunde inte laddas", + + "error.email.preset.notFound": "E-postförinställningen \"{name}\" kan inte hittas", + + "error.field.converter.invalid": "Ogiltig omvandlare \"{converter}\"", + + "error.file.changeName.empty": "Namnet får inte vara tomt", + "error.file.changeName.permission": + "Du har inte behörighet att ändra namnet på \"{filename}\"", + "error.file.duplicate": "En fil med namnet \"{filename}\" existerar redan", + "error.file.extension.forbidden": + "Filändelsen \"{extension}\" är inte tillåten", + "error.file.extension.missing": + "Filen \"{filename}\" saknar filändelse", + "error.file.maxheight": "Bildens höjd får inte överstiga {height} pixlar", + "error.file.maxsize": "Filen är för stor", + "error.file.maxwidth": "Bildens bredd får inte överstiga {width} pixlar", + "error.file.mime.differs": + "Den uppladdade filen måste vara av samma mime-typ \"{mime}\"", + "error.file.mime.forbidden": "Mediatypen \"{mime}\" är inte tillåten", + "error.file.mime.invalid": "Ogiltig mime-typ: {mime}", + "error.file.mime.missing": + "Mediatypen för \"{filename}\" kan inte detekteras", + "error.file.minheight": "Bildens höjd måste vara minst {height} pixlar", + "error.file.minsize": "Filen är för liten", + "error.file.minwidth": "Bildens bredd måste vara minst {width} pixlar", + "error.file.name.missing": "Filnamnet får inte vara tomt", + "error.file.notFound": "Filen \"{filename}\" kan ej hittas", + "error.file.orientation": "Bildens orientering måste vara \"{orientation}\"", + "error.file.type.forbidden": "Du har inte behörighet att ladda upp filer av typen {type}", + "error.file.undefined": "Filen kan inte hittas", + + "error.form.incomplete": "Vänligen åtgärda alla formulärfel...", + "error.form.notSaved": "Formuläret kunde inte sparas", + + "error.language.code": "Ange en giltig kod för språket", + "error.language.duplicate": "Språket finns redan", + "error.language.name": "Ange ett giltigt namn för språket", + + "error.license.format": "Ange en giltig licensnyckel", + "error.license.email": "Ange en giltig e-postadress", + "error.license.verification": "Licensen kunde inte verifieras", + + "error.page.changeSlug.permission": + "Du har inte behörighet att ändra URL-appendixen för \"{slug}\"", + "error.page.changeStatus.incomplete": + "Sidan innehåller fel och kan inte publiceras", + "error.page.changeStatus.permission": + "Statusen för denna sida kan inte ändras", + "error.page.changeStatus.toDraft.invalid": + "Statusen för sidan \"{slug}\" kan inte ändras till utkast", + "error.page.changeTemplate.invalid": + "Mallen för sidan \"{slug}\" kan inte ändras", + "error.page.changeTemplate.permission": + "Du har inte behörighet att ändra mallen för \"{slug}\"", + "error.page.changeTitle.empty": "Titeln får inte vara tom", + "error.page.changeTitle.permission": + "Du har inte behörighet att ändra titeln för \"{slug}\"", + "error.page.create.permission": "Du har inte behörighet att skapa \"{slug}\"", + "error.page.delete": "Sidan \"{slug}\" kan inte raderas", + "error.page.delete.confirm": "Fyll i sidans titel för att bekräfta", + "error.page.delete.hasChildren": + "Sidan har undersidor och kan inte raderas", + "error.page.delete.permission": "Du har inte behörighet att radera \"{slug}\"", + "error.page.draft.duplicate": + "Ett utkast med URL-appendixen \"{slug}\" existerar redan", + "error.page.duplicate": + "En sida med URL-appendixen \"{slug}\" existerar redan", + "error.page.duplicate.permission": "Du har inte behörighet att duplicera \"{slug}\"", + "error.page.notFound": "Sidan \"{slug}\" kan inte hittas", + "error.page.num.invalid": + "Ange ett giltigt nummer för sortering. Numret får inte vara negativt.", + "error.page.slug.invalid": "Ange ett giltigt URL-prefix", + "error.page.sort.permission": "Sidan \"{slug}\" kan inte sorteras", + "error.page.status.invalid": "Sätt en giltig status för sidan", + "error.page.undefined": "Sidan kan inte hittas", + "error.page.update.permission": "Du har inte behörighet att uppdatera \"{slug}\"", + + "error.section.files.max.plural": + "Du får inte lägga till mer än {max} filer till sektionen \"{section}\"", + "error.section.files.max.singular": + "Du får inte lägga till mer än en fil i sektionen \"{section}\"", + "error.section.files.min.plural": + "Sektionen \"{section}\" kräver minst {min} filer", + "error.section.files.min.singular": + "Sektionen \"{section}\" kräver minst en fil", + + "error.section.pages.max.plural": + "Du får inte lägga till mer än {max} sidor till sektionen \"{section}\"", + "error.section.pages.max.singular": + "Du får inte lägga till mer än en sida i sektionen \"{section}\"", + "error.section.pages.min.plural": + "Sektionen \"{section}\" kräver minst {min} sidor", + "error.section.pages.min.singular": + "Sektionen \"{section}\" kräver minst en sida", + + "error.section.notLoaded": "Sektionen \"{name}\" kunde inte laddas", + "error.section.type.invalid": "Sektionstypen \"{type}\" är inte giltig", + + "error.site.changeTitle.empty": "Titeln får inte vara tom", + "error.site.changeTitle.permission": + "Du har inte behörighet att ändra titeln på webbplatsen", + "error.site.update.permission": "Du har inte behörighet att uppdatera webbplatsen", + + "error.template.default.notFound": "Standardmallen existerar inte", + + "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}\"", + "error.user.changeName.permission": + "Du har inte behörighet att ändra namnet för användaren \"{name}\"", + "error.user.changePassword.permission": + "Du har inte behörighet att ändra lösenordet för användaren \"{name}\"", + "error.user.changeRole.lastAdmin": + "Rollen för den återstående adminanvändaren kan inte ändras", + "error.user.changeRole.permission": + "Du har inte behörighet att ändra rollen för användaren \"{name}\"", + "error.user.changeRole.toAdmin": + "Du har inte behörighet att ge någon en administratörsroll", + "error.user.create.permission": "Du har inte behörighet att skapa denna användare", + "error.user.delete": "Användaren kan inte raderas", + "error.user.delete.lastAdmin": "Den återstående administratören kan inte raderas", + "error.user.delete.lastUser": "Den återstående användaren kan inte raderas", + "error.user.delete.permission": + "Du har inte behörighet att radera användaren \"{name}\"", + "error.user.duplicate": + "En användare med e-postadressen \"{email}\" finns redan", + "error.user.email.invalid": "Ange en giltig e-postadress", + "error.user.language.invalid": "Ange ett giltigt språk", + "error.user.notFound": "Användaren \"{name}\" kan ej hittas", + "error.user.password.invalid": + "Ange ett giltigt lösenord. Lösenordet måste vara minst 8 tecken långt.", + "error.user.password.notSame": "Lösenorden matchar inte", + "error.user.password.undefined": "Användaren har inget lösenord", + "error.user.role.invalid": "Ange en giltig roll", + "error.user.update.permission": + "Du har inte behörighet att uppdatera användaren \"{name}\"", + + "error.validation.accepted": "Vänligen bekräfta", + "error.validation.alpha": "Ange endast tecken mellan a-z", + "error.validation.alphanum": + "Ange endast tecken mellan a-z eller siffror 0-9", + "error.validation.between": + "Ange ett värde mellan \"{min}\" och \"{max}\"", + "error.validation.boolean": "Bekräfta eller neka", + "error.validation.contains": + "Ange ett värde som innehåller \"{needle}\"", + "error.validation.date": "Ange ett giltigt datum", + "error.validation.date.after": "Ange ett datum efter {date}", + "error.validation.date.before": "Ange ett datum före {date}", + "error.validation.date.between": "Ange ett datum mellan {min} och {max}", + "error.validation.denied": "Vänligen neka", + "error.validation.different": "Värdet får inte vara \"{other}\"", + "error.validation.email": "Ange en giltig e-postadress", + "error.validation.endswith": "Värdet måste sluta med \"{end}\"", + "error.validation.filename": "Ange ett giltigt filnamn", + "error.validation.in": "Ange ett av följande: ({in})", + "error.validation.integer": "Ange en giltig heltalssiffra", + "error.validation.ip": "Ange en giltig IP-adress", + "error.validation.less": "Ange ett värde lägre än {max}", + "error.validation.match": "Värdet matchar inte det förväntade mönstret", + "error.validation.max": "Ange ett värde som är lika med eller lägre än {max}", + "error.validation.maxlength": + "Ange ett kortare värde. (max {max} tecken)", + "error.validation.maxwords": "Ange inte mer än {max} ord", + "error.validation.min": "Ange ett värde som är lika med eller större än {min}", + "error.validation.minlength": + "Ange ett längre värde. (minst {min} tecken)", + "error.validation.minwords": "Ange minst {min} ord", + "error.validation.more": "Ange ett större värde än {min}", + "error.validation.notcontains": + "Ange ett värde som inte innehåller \"{needle}\"", + "error.validation.notin": + "Ange inte något av följande: ({notIn})", + "error.validation.option": "Välj ett giltigt alternativ", + "error.validation.num": "Ange ett giltigt nummer", + "error.validation.required": "Ange någonting", + "error.validation.same": "Ange \"{other}\"", + "error.validation.size": "Storleken av värdet måste vara \"{size}\"", + "error.validation.startswith": "Värdet måste börja med \"{start}\"", + "error.validation.time": "Ange en giltig tid", + "error.validation.url": "Ange en giltig URL", + + "field.required": "Fältet krävs", + "field.files.empty": "Inga filer valda än", + "field.pages.empty": "Inga sidor valda än", + "field.structure.delete.confirm": "Vill du verkligen radera denna rad?", + "field.structure.empty": "Inga poster än", + "field.users.empty": "Inga användare valda än", + + "file.delete.confirm": + "Vill du verkligen radera
{filename}?", + + "files": "Filer", + "files.empty": "Inga filer än", + + "hour": "Timme", + "insert": "Infoga", + "install": "Installera", + + "installation": "Installation", + "installation.completed": "Panelen har installerats", + "installation.disabled": "Installeraren för panelen är som standard inaktiverad på offentliga servrar. Kör installeraren på en lokal maskin eller aktivera den med alternativet panel.install.", + "installation.issues.accounts": + "Mappen /site/accounts finns inte eller är inte skrivbar", + "installation.issues.content": + "Mappen /content finns inte eller är inte skrivbar", + "installation.issues.curl": "Tillägget CURL krävs", + "installation.issues.headline": "Panelen kan inte installeras", + "installation.issues.mbstring": + "Tillägget MB String krävs", + "installation.issues.media": + "Mappen /media finns inte eller är inte skrivbar", + "installation.issues.php": "Se till att du använder PHP 7+", + "installation.issues.server": + "Kirby kräver Apache, Nginx eller Caddy", + "installation.issues.sessions": "Mappen /site/sessions finns inte eller är inte skrivbar", + + "language": "Spr\u00e5k", + "language.code": "Kod", + "language.convert": "Ange som standard", + "language.convert.confirm": + "

Vill du verkligen göra {name} till standardspråket? Detta kan inte ångras.

Om {name} har oöversatt innehåll, kommer det inte längre finnas en alternativ översättning och delar av sajten kommer kanske att vara tom.

", + "language.create": "Lägg till ett nytt språk", + "language.delete.confirm": + "Vill du verkligen radera språket {name} inklusive alla översättningar? Detta kan inte ångras!", + "language.deleted": "Språket har raderats", + "language.direction": "Läsriktning", + "language.direction.ltr": "Vänster till höger", + "language.direction.rtl": "Höger till vänster", + "language.locale": "PHP locale string", + "language.locale.warning": "Du använder en anpassad språkinställning. Ändra den i språkfilen i mappen /site/languages", + "language.name": "Namn", + "language.updated": "Språket har uppdaterats", + + "languages": "Språk", + "languages.default": "Standardspråk", + "languages.empty": "Det finns inga språk ännu", + "languages.secondary": "Sekundära språk", + "languages.secondary.empty": "Det finns inga sekundära språk ännu", + + "license": "Licens", + "license.buy": "Köp en licens", + "license.register": "Registrera", + "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", + + "link": "L\u00e4nk", + "link.text": "L\u00e4nktext", + + "loading": "Laddar", + + "lock.unsaved": "Osparade ändringar", + "lock.unsaved.empty": "Det finns inga fler osparade ändringar", + "lock.isLocked": "Osparade ändringar av {email}", + "lock.file.isLocked": "Filen redigeras just nu av {email} och kan inte redigeras.", + "lock.page.isLocked": "Sidan redigeras just nu av {email} och kan inte redigeras.", + "lock.unlock": "Lås upp", + "lock.isUnlocked": "Dina osparade ändringar har skrivits över av en annan användare. Du kan ladda ner dina ändringar för att slå ihop dem manuellt.", + + "login": "Logga in", + "login.remember": "Håll mig inloggad", + + "logout": "Logga ut", + + "menu": "Meny", + "meridiem": "a.m./p.m.", + "mime": "Mediatyp", + "minutes": "Minuter", + + "month": "Månad", + "months.april": "April", + "months.august": "Augusti", + "months.december": "December", + "months.february": "Februari", + "months.january": "Januari", + "months.july": "Juli", + "months.june": "Juni", + "months.march": "Mars", + "months.may": "Maj", + "months.november": "November", + "months.october": "Oktober", + "months.september": "September", + + "more": "Mer", + "name": "Namn", + "next": "Nästa", + "off": "av", + "on": "på", + "open": "Öppna", + "options": "Alternativ", + + "orientation": "Orientering", + "orientation.landscape": "Liggande", + "orientation.portrait": "Stående", + "orientation.square": "Kvadrat", + + "page.changeSlug": "Ändra URL", + "page.changeSlug.fromTitle": "Skapa utifr\u00e5n titel", + "page.changeStatus": "Ändra status", + "page.changeStatus.position": "Välj en ny position", + "page.changeStatus.select": "Välj en ny status", + "page.changeTemplate": "Ändra mall", + "page.delete.confirm": + "Vill du verkligen radera {title}?", + "page.delete.confirm.subpages": + "Denna sida har undersidor.
Alla undersidor kommer också att raderas.", + "page.delete.confirm.title": "Fyll i sidans titel för att bekräfta", + "page.draft.create": "Skapa utkast", + "page.duplicate.appendix": "Kopiera", + "page.duplicate.files": "Kopiera filer", + "page.duplicate.pages": "Kopiera sidor", + "page.status": "Status", + "page.status.draft": "Utkast", + "page.status.draft.description": + "Sidan är ett utkast och endast synlig för inloggade redaktörer", + "page.status.listed": "Publika", + "page.status.listed.description": "Sidan är publik för vem som helst", + "page.status.unlisted": "Olistade", + "page.status.unlisted.description": "Sidan är endast åtkomlig via URL", + + "pages": "Sidor", + "pages.empty": "Inga sidor än", + "pages.status.draft": "Utkast", + "pages.status.listed": "Publicerade", + "pages.status.unlisted": "Olistade", + + "pagination.page": "Sida", + + "password": "L\u00f6senord", + "pixel": "Pixel", + "prev": "Föregående", + "remove": "Ta bort", + "rename": "Byt namn", + "replace": "Ersätt", + "retry": "F\u00f6rs\u00f6k igen", + "revert": "Återgå", + + "role": "Roll", + "role.admin.description": "Administratören har alla behörigheter", + "role.admin.title": "Administratör", + "role.all": "Alla", + "role.empty": "Det finns inga användare med denna roll", + "role.description.placeholder": "Ingen beskrivning", + "role.nobody.description": "Detta är en roll utan några behörigheter", + "role.nobody.title": "Ingen", + + "save": "Spara", + "search": "Sök", + + "section.required": "Sektionen krävs", + + "select": "Välj", + "settings": "Inställningar", + "size": "Storlek", + "slug": "URL-appendix", + "sort": "Sortera", + "title": "Titel", + "template": "Mall", + "today": "Idag", + + "toolbar.button.code": "Kod", + "toolbar.button.bold": "Fet", + "toolbar.button.email": "E-post", + "toolbar.button.headings": "Rubriker", + "toolbar.button.heading.1": "Rubrik 1", + "toolbar.button.heading.2": "Rubrik 2", + "toolbar.button.heading.3": "Rubrik 3", + "toolbar.button.italic": "Kursiv", + "toolbar.button.file": "Fil", + "toolbar.button.file.select": "Välj en fil", + "toolbar.button.file.upload": "Ladda upp en fil", + "toolbar.button.link": "L\u00e4nk", + "toolbar.button.ol": "Sorterad lista", + "toolbar.button.ul": "Punktlista", + + "translation.author": "Kirby-teamet, Ola Christensson", + "translation.direction": "ltr", + "translation.name": "Svenska", + "translation.locale": "sv_SE", + + "upload": "Ladda upp", + "upload.error.cantMove": "Den överförda filen kunde inte flyttas", + "upload.error.cantWrite": "Det gick inte att skriva filen till hårddisken", + "upload.error.default": "Filen kunde inte laddas upp", + "upload.error.extension": "Filuppladdningen förhindrades på grund av filändelsen", + "upload.error.formSize": "Den överförda filen överskrider den maximala filstorlek som anges i formuläret (MAX_FILE_SIZE)", + "upload.error.iniPostSize": "Den överförda filen överskrider post_max_size-direktivet i php.ini", + "upload.error.iniSize": "Den överförda filen överskrider direktivet upload_max_filesize i php.ini", + "upload.error.noFile": "Ingen fil laddades upp", + "upload.error.noFiles": "Inga filer laddades upp", + "upload.error.partial": "Den överförda filen laddades bara delvis upp", + "upload.error.tmpDir": "Saknar en temporär mapp", + "upload.errors": "Fel", + "upload.progress": "Laddar upp...", + + "url": "URL", + "url.placeholder": "https://exempel.se", + + "user": "Användare", + "user.blueprint": + "Du kan ange ytterligare sektioner och fält för denna användarroll i /site/blueprints/users/{role}.yml", + "user.changeEmail": "Ändra e-postadress", + "user.changeLanguage": "Ändra språk", + "user.changeName": "Byt namn på denna användare", + "user.changePassword": "Ändra lösenord", + "user.changePassword.new": "Nytt lösenord", + "user.changePassword.new.confirm": "Bekräfta det nya lösenordet...", + "user.changeRole": "Ändra roll", + "user.changeRole.select": "Välj en ny roll", + "user.create": "Lägg till en ny användare", + "user.delete": "Radera denna användare", + "user.delete.confirm": + "Vill du verkligen radera
{email}?", + + "users": "Användare", + + "version": "Version", + + "view.account": "Ditt konto", + "view.installation": "Installation", + "view.settings": "Inställningar", + "view.site": "Webbplats", + "view.users": "Anv\u00e4ndare", + + "welcome": "Välkommen", + "year": "År" +} diff --git a/kirby/i18n/translations/tr.json b/kirby/i18n/translations/tr.json new file mode 100755 index 0000000..5d8e2f2 --- /dev/null +++ b/kirby/i18n/translations/tr.json @@ -0,0 +1,481 @@ +{ + "add": "Ekle", + "avatar": "Profil resmi", + "back": "Geri", + "cancel": "\u0130ptal", + "change": "De\u011fi\u015ftir", + "close": "Kapat", + "confirm": "Tamam", + "copy": "Kopyala", + "create": "Oluştur", + + "date": "Tarih", + "date.select": "Bir tarih seçiniz", + + "day": "Gün", + "days.fri": "Cum", + "days.mon": "Pzt", + "days.sat": "Cmt", + "days.sun": "Paz", + "days.thu": "Per", + "days.tue": "Sal", + "days.wed": "\u00c7ar", + + "delete": "Sil", + "dimensions": "Boyutlar", + "disabled": "Devredışı", + "discard": "Vazge\u00e7", + "download": "İndir", + "duplicate": "Kopyala", + "edit": "D\u00fczenle", + + "dialog.files.empty": "Seçilecek dosya yok", + "dialog.pages.empty": "Seçilecek sayfa yok", + "dialog.users.empty": "Seçilecek kullanıcı yok", + + "email": "E-Posta", + "email.placeholder": "eposta@ornek.com", + + "error.access.login": "Geçersiz giriş", + "error.access.panel": "Panele erişim izniniz yok", + "error.access.view": "Panelin bu bölümüne erişemezsiniz", + + "error.avatar.create.fail": "Profil resmi yüklenemedi", + "error.avatar.delete.fail": "Profil resmi silinemedi", + "error.avatar.dimensions.invalid": + "Lütfen profil resminin genişliğini ve yüksekliğini 3000 pikselin altında tutun", + "error.avatar.mime.forbidden": + "Profil resmi JPEG veya PNG dosyaları olmalıdır", + + "error.blueprint.notFound": "\"{name}\" adlı plan yüklenemedi", + + "error.email.preset.notFound": "\"{name}\" e-posta adresi bulunamadı", + + "error.field.converter.invalid": "Geçersiz dönüştürücü \"{converter}\"", + + "error.file.changeName.empty": "İsim boş olmamalıdır", + "error.file.changeName.permission": + "\"{filename}\" adını değiştiremezsiniz", + "error.file.duplicate": "\"{filename}\" isimli bir dosya zaten var", + "error.file.extension.forbidden": + "\"{extension}\" dosya uzantısına izin verilmiyor", + "error.file.extension.missing": + "\"{filename}\" dosyasının uzantısı yok", + "error.file.maxheight": "Resmin yüksekliği {height} pikselden büyük olmamalıdır", + "error.file.maxsize": "Dosya çok büyük", + "error.file.maxwidth": "Resmin genişliği {width} pikselden büyük olmamalıdır", + "error.file.mime.differs": + "Yüklenen dosya aynı dosya türü \"{mime}\" olmalıdır", + "error.file.mime.forbidden": "\"{mime}\" medya türüne izin verilmiyor", + "error.file.mime.invalid": "Geçersiz medya türü: {mime}", + "error.file.mime.missing": + "\"{filename}\" için medya türü tespit edilemiyor", + "error.file.minheight": "Resmin yüksekliği en az {height} piksel olmalıdır", + "error.file.minsize": "Dosya çok küçük", + "error.file.minwidth": "Resmin genişliği en az {width} piksel olmalıdır", + "error.file.name.missing": "Dosya adı boş bırakılamaz", + "error.file.notFound": "\"{filename}\" dosyası bulunamadı", + "error.file.orientation": "Resmin oryantasyonu \"{orientation}\" olmalıdır", + "error.file.type.forbidden": "{type} dosya yükleme izni yok", + "error.file.undefined": "Dosya bulunamad\u0131", + + "error.form.incomplete": "Lütfen tüm form hatalarını düzeltin...", + "error.form.notSaved": "Form kaydedilemedi", + + "error.language.code": "Lütfen dil için geçerli bir kod girin", + "error.language.duplicate": "Bu dil zaten var", + "error.language.name": "Lütfen dil için geçerli bir isim girin", + + "error.license.format": "Lütfen geçerli bir lisans anahtarı girin", + "error.license.email": "Lütfen geçerli bir e-posta adresi girin", + "error.license.verification": "Lisans doğrulanamadı", + + "error.page.changeSlug.permission": + "\"{slug}\" uzantısına sahip bu sayfanın adresini değiştirilemez", + "error.page.changeStatus.incomplete": + "Sayfada hatalar var ve yayınlanamadı", + "error.page.changeStatus.permission": + "Bu sayfanın durumu değiştirilemez", + "error.page.changeStatus.toDraft.invalid": + "\"{slug}\" sayfası bir taslak haline dönüştürülemiyor", + "error.page.changeTemplate.invalid": + "\"{slug}\" sayfası için şablon değiştirilemiyor", + "error.page.changeTemplate.permission": + "\"{slug}\" için şablonu değiştiremezsiniz", + "error.page.changeTitle.empty": "Başlık boş bırakılamaz", + "error.page.changeTitle.permission": + "\"{slug}\" için başlığı değiştiremezsiniz", + "error.page.create.permission": "\"{slug}\" oluşturmanıza izin verilmiyor", + "error.page.delete": "\"{slug}\" sayfası silinemedi", + "error.page.delete.confirm": "Onaylamak için sayfa başlığını girin", + "error.page.delete.hasChildren": + "Sayfada alt sayfalar var ve silinemiyor", + "error.page.delete.permission": "\"{slug}\" öğesini silmenize izin verilmiyor", + "error.page.draft.duplicate": + "\"{slug}\" adres eki olan bir sayfa taslağı zaten mevcut", + "error.page.duplicate": + "\"{slug}\" adres eki içeren bir sayfa zaten mevcut", + "error.page.duplicate.permission": "\"{slug}\" öğesini çoğaltmanıza izin verilmiyor", + "error.page.notFound": "\"{slug}\" uzantısındaki sayfa bulunamadı", + "error.page.num.invalid": + "Lütfen geçerli bir sıralama numarası girin. Sayılar negatif olmamalıdır.", + "error.page.slug.invalid": "Lütfen geçerli bir adres öneki girin", + "error.page.sort.permission": "\"{slug}\" sayfası sıralanamıyor", + "error.page.status.invalid": "Lütfen geçerli bir sayfa durumu ayarlayın", + "error.page.undefined": "Sayfa bulunamad\u0131", + "error.page.update.permission": "\"{slug}\" güncellemesine izin verilmiyor", + + "error.section.files.max.plural": + "\"{section}\" bölümüne {max} dosyadan daha fazlasını eklememelisiniz", + "error.section.files.max.singular": + "\"{section}\" bölümüne birden fazla dosya eklememelisiniz", + "error.section.files.min.plural": + "\"{section}\" bölümü en az {min} dosya gerektiriyor", + "error.section.files.min.singular": + "\"{section}\" bölümü en az bir dosya gerektiriyor", + + "error.section.pages.max.plural": + "\"{section}\" bölümüne maksimum {max} sayfadan fazla ekleyemezsiniz", + "error.section.pages.max.singular": + "\"{section}\" bölümüne birden fazla sayfa ekleyemezsiniz", + "error.section.pages.min.plural": + "\"{section}\" bölümü en az {min} sayfa gerektiriyor", + "error.section.pages.min.singular": + "\"{section}\" bölümü en az bir sayfa gerektiriyor", + + "error.section.notLoaded": "\"{name}\" bölümü yüklenemedi", + "error.section.type.invalid": "\"{type}\" tipi geçerli değil", + + "error.site.changeTitle.empty": "Başlık boş bırakılamaz", + "error.site.changeTitle.permission": + "Sitenin başlığını değiştiremezsin", + "error.site.update.permission": "Siteyi güncellemenize izin verilmiyor", + + "error.template.default.notFound": "Varsayılan şablon yok", + + "error.user.changeEmail.permission": + "\"{name}\" kullanıcısı için e-postayı değiştiremezsiniz", + "error.user.changeLanguage.permission": + "\"{name}\" kullanıcısının dilini değiştiremezsin", + "error.user.changeName.permission": + "\"{name}\" kullanıcısının adını değiştiremezsiniz", + "error.user.changePassword.permission": + "\"{name}\" kullanıcısının şifresini değiştiremezsiniz", + "error.user.changeRole.lastAdmin": + "Son yöneticinin rolü değiştirilemez", + "error.user.changeRole.permission": + "\"{name}\" kullanıcısının rolünü değiştiremezsin", + "error.user.changeRole.toAdmin": + "Birini yönetici rolüne tanıtmanıza izin verilmiyor", + "error.user.create.permission": "Bu kullanıcıyı oluşturmanıza izin verilmiyor", + "error.user.delete": "\"{name}\" kullanıcısı silinemedi", + "error.user.delete.lastAdmin": "Son y\u00f6netici kullan\u0131c\u0131y\u0131 silemezsiniz", + "error.user.delete.lastUser": "Son kullanıcı silinemez", + "error.user.delete.permission": + "\"{name}\" kullanıcısını silme yetkiniz yok", + "error.user.duplicate": + "\"{email}\" e-posta adresine sahip bir kullanıcı zaten var", + "error.user.email.invalid": "Lütfen geçerli bir e-posta adresi girin", + "error.user.language.invalid": "Lütfen geçerli bir dil girin", + "error.user.notFound": "\"{name}\" kullanıcısı bulunamadı", + "error.user.password.invalid": + "Lütfen geçerli bir şifre giriniz. Şifreler en az 8 karakter uzunluğunda olmalıdır.", + "error.user.password.notSame": "L\u00fctfen \u015fifreyi do\u011frulay\u0131n", + "error.user.password.undefined": "Bu kullanıcının şifresi yok", + "error.user.role.invalid": "Lütfen geçerli bir rol girin", + "error.user.update.permission": + "\"{name}\" kullanıcısını güncellemenize izin verilmiyor", + + "error.validation.accepted": "Lütfen onaylayın", + "error.validation.alpha": "Lütfen sadece a-z arasındaki karakterleri girin", + "error.validation.alphanum": + "Lütfen sadece a-z veya 0-9 arasındaki rakamları girin", + "error.validation.between": + "Lütfen \"{min}\" ile \"{max}\" arasında bir değer girin", + "error.validation.boolean": "Lütfen onaylayın veya reddedin", + "error.validation.contains": + "Lütfen \"{needle}\" içeren bir değer girin", + "error.validation.date": "Lütfen geçerli bir tarih girin", + "error.validation.date.after": "Lütfen {date} tarihinden sonra bir tarih girin", + "error.validation.date.before": "Lütfen {date} tarihinden önce bir tarih girin", + "error.validation.date.between": "Lütfen {min} ve {max} arasında bir tarih girin", + "error.validation.denied": "Lütfen reddedin", + "error.validation.different": "Değer \"{other}\" olmamalıdır", + "error.validation.email": "Lütfen geçerli bir e-posta adresi girin", + "error.validation.endswith": "Değer \"{end}\" ile bitmelidir", + "error.validation.filename": "Lütfen geçerli bir dosya adı girin", + "error.validation.in": "Lütfen bunlardan birini girin: ({in})", + "error.validation.integer": "Lütfen geçerli bir tamsayı girin", + "error.validation.ip": "Lütfen geçerli bir ip adresi girin", + "error.validation.less": "Lütfen {max} 'dan daha düşük bir değer girin", + "error.validation.match": "Değer beklenen modelle eşleşmiyor", + "error.validation.max": "Lütfen {max} 'a eşit veya daha küçük bir değer girin", + "error.validation.maxlength": + "Lütfen daha kısa bir değer girin. (maks. {max} karakter)", + "error.validation.maxwords": "Lütfen en fazla {max} kelime(ler) girin", + "error.validation.min": "Lütfen {min} ile eşit veya daha büyük bir değer girin", + "error.validation.minlength": + "Lütfen daha uzun bir değer girin. (min. {min} karakter)", + "error.validation.minwords": "Lütfen en az {min} kelime(ler) girin", + "error.validation.more": "Lütfen {min} değerinden daha büyük bir değer girin", + "error.validation.notcontains": + "Lütfen \"{needle}\" içermeyen bir değer girin", + "error.validation.notin": + "Lütfen bunlardan herhangi birini girmeyin: ({notIn})", + "error.validation.option": "Lütfen geçerli bir seçenek girin", + "error.validation.num": "Lütfen geçerli bir sayı girin", + "error.validation.required": "Lütfen birşeyler girin", + "error.validation.same": "Lütfen \"{other}\" yazınız", + "error.validation.size": "Değerin boyutu \"{size}\" olmalıdır", + "error.validation.startswith": "Değer \"{start}\" ile başlamalıdır", + "error.validation.time": "Lütfen geçerli bir zaman girin", + "error.validation.url": "Lütfen geçerli bir adres girin", + + "field.required": "Alan gereklidir", + "field.files.empty": "Henüz dosya seçilmedi", + "field.pages.empty": "Henüz sayfa seçilmedi", + "field.structure.delete.confirm": "Bu girdiyi silmek istedi\u011finizden emin misiniz?", + "field.structure.empty": "Hen\u00fcz bir girdi yok", + "field.users.empty": "Henüz kullanıcı seçilmedi", + + "file.delete.confirm": + "{filename} dosyasını silmek istediğinizden emin misiniz?", + + "files": "Dosyalar", + "files.empty": "Henüz dosya yok", + + "hour": "Saat", + "insert": "Ekle", + "install": "Kurulum", + + "installation": "Kurulum", + "installation.completed": "Panel kuruldu", + "installation.disabled": "Panel yükleyici, herkese açık sunucularda varsayılan olarak devre dışıdır. Lütfen yükleyiciyi yerel bir makinede çalıştırın veya panel.install seçeneğiyle etkinleştirin.", + "installation.issues.accounts": + "/site/accounts klasörü yok yada yazılabilir değil", + "installation.issues.content": + "/content klasörü yok yada yazılabilir değil", + "installation.issues.curl": "CURL eklentisi gerekli", + "installation.issues.headline": "Panel kurulamadı", + "installation.issues.mbstring": + "MB String eklentisi gerekli", + "installation.issues.media": + "/media klasörü yok yada yazılamaz", + "installation.issues.php": "PHP 7+ kullandığınızdan emin olun. ", + "installation.issues.server": + "Kirby Apache, Nginx or Caddy gerektirir", + "installation.issues.sessions": "/site/sessions klasörü mevcut değil veya yazılabilir değil", + + "language": "Dil", + "language.code": "Kod", + "language.convert": "Varsayılan yap", + "language.convert.confirm": + "

{name}'i varsayılan dile dönüştürmek istiyor musunuz? Bu geri alınamaz.

{name} çevrilmemiş içeriğe sahipse, artık geçerli bir geri dönüş olmaz ve sitenizin bazı bölümleri boş olabilir.

", + "language.create": "Yeni bir dil ekle", + "language.delete.confirm": + "Tüm çevirileri içeren {name} dilini gerçekten silmek istiyor musunuz? Bu geri alınamaz!", + "language.deleted": "Dil silindi", + "language.direction": "Okuma yönü", + "language.direction.ltr": "Soldan sağa", + "language.direction.rtl": "Sağdan sola", + "language.locale": "PHP yerel dizesi", + "language.locale.warning": "Özel bir yerel ayar kullanıyorsunuz. Lütfen /site/languages konumundaki dil dosyasından değiştirin.", + "language.name": "İsim", + "language.updated": "Dil güncellendi", + + "languages": "Diller", + "languages.default": "Varsayılan dil", + "languages.empty": "Henüz hiç dil yok", + "languages.secondary": "İkincil diller", + "languages.secondary.empty": "Henüz ikincil bir dil yok", + + "license": "Lisans", + "license.buy": "Bir lisans satın al", + "license.register": "Kayıt Ol", + "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", + + "link": "Ba\u011flant\u0131", + "link.text": "Ba\u011flant\u0131 yaz\u0131s\u0131", + + "loading": "Yükleniyor", + + "lock.unsaved": "Kaydedilmemiş değişiklikler", + "lock.unsaved.empty": "Daha fazla kaydedilmemiş değişiklik yok", + "lock.isLocked": "{email} tarafından kaydedilmemiş değişiklikler", + "lock.file.isLocked": "Dosya şu anda {email} tarafından düzenlenmektedir ve değiştirilemez.", + "lock.page.isLocked": "Sayfa şu anda {email} tarafından düzenlenmektedir ve değiştirilemez.", + "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.remember": "Oturumumu açık tut", + + "logout": "Güvenli Çıkış", + + "menu": "Menü", + "meridiem": "AM/PM", + "mime": "Medya Türü", + "minutes": "Dakika", + + "month": "Ay", + "months.april": "Nisan", + "months.august": "A\u011fustos", + "months.december": "Aral\u0131k", + "months.february": "\u015eubat", + "months.january": "Ocak", + "months.july": "Temmuz", + "months.june": "Haziran", + "months.march": "Mart", + "months.may": "May\u0131s", + "months.november": "Kas\u0131m", + "months.october": "Ekim", + "months.september": "Eyl\u00fcl", + + "more": "Daha Fazla", + "name": "İsim", + "next": "Sonraki", + "off": "kapalı", + "on": "açık", + "open": "Önizleme", + "options": "Seçenekler", + + "orientation": "Oryantasyon", + "orientation.landscape": "Yatay", + "orientation.portrait": "Dikey", + "orientation.square": "Kare", + + "page.changeSlug": "Web Adresini Değiştir", + "page.changeSlug.fromTitle": "Ba\u015fl\u0131ktan olu\u015ftur", + "page.changeStatus": "Durumu değiştir", + "page.changeStatus.position": "Lütfen bir pozisyon seçin", + "page.changeStatus.select": "Yeni bir durum seçin", + "page.changeTemplate": "Şablonu değiştir", + "page.delete.confirm": + "{title} sayfasını silmek istediğinizden emin misiniz?", + "page.delete.confirm.subpages": + "Bu sayfada alt sayfalar var.
Tüm alt sayfalar da silinecek.", + "page.delete.confirm.title": "Onaylamak için sayfa başlığını girin", + "page.draft.create": "Taslak oluştur", + "page.duplicate.appendix": "Kopya", + "page.duplicate.files": "Dosyaları kopyala", + "page.duplicate.pages": "Sayfaları kopyala", + "page.status": "Durum", + "page.status.draft": "Taslak", + "page.status.draft.description": + "Sayfa taslak modunda ve sadece giriş yapılan düzenleyiciler için görünür durumda", + "page.status.listed": "Herkese Açık", + "page.status.listed.description": "Bu sayfa herkese açık", + "page.status.unlisted": "Liste Dışı", + "page.status.unlisted.description": "Bu sayfa sadece bağlantı adresi ile erişilebilir", + + "pages": "Sayfalar", + "pages.empty": "Henüz sayfa yok", + "pages.status.draft": "Taslaklar", + "pages.status.listed": "Yayınlandı", + "pages.status.unlisted": "Liste Dışı", + + "pagination.page": "Sayfa", + + "password": "\u015eifre", + "pixel": "Piksel", + "prev": "Önceki", + "remove": "Kaldır", + "rename": "Yeniden Adlandır", + "replace": "De\u011fi\u015ftir", + "retry": "Tekrar Dene", + "revert": "Vazge\u00e7", + + "role": "Rol", + "role.admin.description": "Yönetici tüm haklara sahiptir", + "role.admin.title": "Yönetici", + "role.all": "Tümü", + "role.empty": "Bu role ait kullanıcı bulunamadı", + "role.description.placeholder": "Açıklama yok", + "role.nobody.description": "Bu hiçbir izni olmayan bir geri dönüş rolüdür.", + "role.nobody.title": "Hiçkimse", + + "save": "Kaydet", + "search": "Arama", + + "section.required": "Bölüm gereklidir", + + "select": "Seç", + "settings": "Ayarlar", + "size": "Boyut", + "slug": "Web Adres Uzantısı", + "sort": "Sırala", + "title": "Başlık", + "template": "\u015eablon", + "today": "Bugün", + + "toolbar.button.code": "Kod", + "toolbar.button.bold": "Kalın Yazı", + "toolbar.button.email": "E-Posta", + "toolbar.button.headings": "Başlıklar", + "toolbar.button.heading.1": "Başlık 1", + "toolbar.button.heading.2": "Başlık 2", + "toolbar.button.heading.3": "Başlık 3", + "toolbar.button.italic": "Eğik Yazı", + "toolbar.button.file": "Dosya", + "toolbar.button.file.select": "Bir dosya seçin", + "toolbar.button.file.upload": "Bir dosya yükleyin", + "toolbar.button.link": "Ba\u011flant\u0131", + "toolbar.button.ol": "Sıralı liste", + "toolbar.button.ul": "Madde listesi", + + "translation.author": "Kirby Takımı", + "translation.direction": "ltr", + "translation.name": "T\u00fcrk\u00e7e", + "translation.locale": "tr_TR", + + "upload": "Yükle", + "upload.error.cantMove": "Yüklenen dosya taşınamadı", + "upload.error.cantWrite": "Dosya diske yazılamadı", + "upload.error.default": "Dosya yüklenemedi", + "upload.error.extension": "Dosya yükleme uzantısı tarafından durduruldu", + "upload.error.formSize": "Yüklenen dosya, formda belirtilen MAX_FILE_SIZE yönergesini aşıyor", + "upload.error.iniPostSize": "Yüklenen dosya php.ini içindeki post_max_size yönergesini aşıyor", + "upload.error.iniSize": "Yüklenen dosya php.ini içindeki upload_max_filesize yönergesini aşıyor", + "upload.error.noFile": "Dosya yüklenmedi", + "upload.error.noFiles": "Dosyalar yüklenmedi", + "upload.error.partial": "Yüklenen dosya sadece kısmen yüklendi", + "upload.error.tmpDir": "Geçici klasör eksik", + "upload.errors": "Hata", + "upload.progress": "Yükleniyor...", + + "url": "Url", + "url.placeholder": "https://ornek.com", + + "user": "Kullanıcı", + "user.blueprint": + "Bu kullanıcı rolü için /site/blueprints/users/{role}.yml içinde ek bölümler ve form alanları tanımlayabilirsiniz", + "user.changeEmail": "E-postayı değiştir", + "user.changeLanguage": "Dili değiştir", + "user.changeName": "Kullanıcıyı yeniden adlandır", + "user.changePassword": "Şifre değiştir", + "user.changePassword.new": "Yeni Şifre", + "user.changePassword.new.confirm": "Şifreyi onaylayın...", + "user.changeRole": "Rolü değiştir", + "user.changeRole.select": "Yeni bir rol seçin", + "user.create": "Yeni bir kullanıcı ekle", + "user.delete": "Bu kullanıcıyı sil", + "user.delete.confirm": + "{email} kullanıcısını silmek istediğinizden emin misiniz?", + + "users": "Kullanıcılar", + + "version": "Versiyon", + + "view.account": "Hesap Bilgilerin", + "view.installation": "Kurulum", + "view.settings": "Ayarlar", + "view.site": "Site", + "view.users": "Kullan\u0131c\u0131lar", + + "welcome": "Hoşgeldiniz", + "year": "Yıl" +} diff --git a/kirby/kirby.pub b/kirby/kirby.pub new file mode 100755 index 0000000..ddf9130 --- /dev/null +++ b/kirby/kirby.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Ux4q7LmQ5hfTYTtz3/a +mohFJMWo/iCnxVcY84PZjLwWnT+G2DTKGaEWydB77TteJQnmsgtvO5734oj3Ga3r +QCfwr2gxo/0WDEBq7C5HP+YNJiuZ/iD/tYV+gloF+Aaa3Mo8AK5DYH3dnjuyfHc1 +veIlYX1D2MXji2IRqdweAzVi1dfI4I3Ys8awhzv653vFLj5LvAtlwlYlmYeRwci7 +GkAOWw709CuKQNdPBXGFQQ/pEB5mnp8mI31j8og845u6v/Sk4+85gFORSufIRfnQ +GFYrPOeavxfAWQGjh7JQjr/sbKSXaJ3nDlrYsOPIrC0Rwn/jsQPO7OLdVwkc9ofL +GQIDAQAB +-----END PUBLIC KEY----- diff --git a/kirby/panel/dist/apple-touch-icon.png b/kirby/panel/dist/apple-touch-icon.png new file mode 100755 index 0000000..832510e Binary files /dev/null and b/kirby/panel/dist/apple-touch-icon.png differ diff --git a/kirby/panel/dist/css/app.css b/kirby/panel/dist/css/app.css new file mode 100755 index 0000000..36a8a4d --- /dev/null +++ b/kirby/panel/dist/css/app.css @@ -0,0 +1 @@ +*,:after,:before{margin:0;padding:0;-webkit-box-sizing:border-box;box-sizing:border-box}:root{--color-backdrop:rgba(22,23,26,0.6);--color-background:#efefef;--color-border:#ccc;--color-focus:#4271ae;--color-focus-light:#81a2be;--color-focus-outline:rgba(66,113,174,0.25);--color-negative:#c82829;--color-negative-light:#d16464;--color-negative-outline:rgba(200,40,41,0.25);--color-notice:#f5871f;--color-notice-light:#de935f;--color-positive:#5d800d;--color-positive-light:#a7bd68;--color-positive-outline:rgba(93,128,13,0.25);--color-text:#16171a;--color-text-light:#777;--font-family-mono:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;--font-family-sans:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;--font-size-tiny:0.75rem;--font-size-small:0.875rem;--font-size-medium:1rem;--font-size-large:1.25rem;--font-size-huge:1.5rem;--font-size-monster:1.75rem;--box-shadow-dropdown:rgba(22,23,26,0.2) 0 2px 10px;--box-shadow-item:rgba(22,23,26,0.05) 0 2px 5px;--box-shadow-focus:#4271ae 0 0 0 2px,rgba(66,113,174,0.2) 0 0 0 2px}noscript{padding:1.5rem;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;height:100vh;text-align:center}html{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;background:#efefef}body,html{color:#16171a;overflow:hidden;height:100%}a{color:inherit;text-decoration:none}li{list-style:none}b,strong{font-weight:600}.fade-enter-active,.fade-leave-active{-webkit-transition:opacity .5s;transition:opacity .5s}.fade-enter,.fade-leave-to{opacity:0}.k-panel{position:absolute;top:0;right:0;bottom:0;left:0;background:#efefef}.k-panel[data-loading]{-webkit-animation:LoadingCursor .5s;animation:LoadingCursor .5s}.k-panel-header{position:absolute;top:0;left:0;right:0;z-index:300}.k-panel .k-form-buttons{position:fixed;bottom:0;left:0;right:0;z-index:300}.k-panel-view{position:absolute;top:0;right:0;bottom:0;left:0;padding-bottom:6rem;overflow-y:scroll;-webkit-overflow-scrolling:touch;-webkit-transform:translateZ(0);transform:translateZ(0)}.k-panel[data-dialog] .k-panel-view{overflow:hidden;-webkit-transform:none;transform:none}.k-panel[data-topbar] .k-panel-view{top:2.5rem}.k-panel[data-dragging],.k-panel[data-loading]:after{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.k-offline-warning{position:fixed;content:" ";top:0;right:0;bottom:0;left:0;z-index:900;background:rgba(22,23,26,.7);content:"offline";display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;color:#fff}@-webkit-keyframes LoadingCursor{to{cursor:progress}}@keyframes LoadingCursor{to{cursor:progress}}@-webkit-keyframes Spin{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes Spin{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.k-offscreen{-webkit-clip-path:inset(100%);clip-path:inset(100%);clip:rect(1px,1px,1px,1px);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}.k-icons{position:absolute;width:0;height:0}[data-invalid]{border:1px solid rgba(200,40,41,.25);-webkit-box-shadow:rgba(200,40,41,.25) 0 0 3px 2px;box-shadow:0 0 3px 2px rgba(200,40,41,.25)}[data-invalid]:focus-within{border:1px solid #c82829!important;-webkit-box-shadow:rgba(200,40,41,.25) 0 0 0 2px!important;box-shadow:0 0 0 2px rgba(200,40,41,.25)!important}.k-dialog{-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;position:fixed;top:0;right:0;bottom:0;left:0;border:0;width:100%;height:100%;background:rgba(22,23,26,.6);z-index:600;-webkit-transform:translateZ(0);transform:translateZ(0)}.k-dialog,.k-dialog-box{display:-webkit-box;display:-ms-flexbox;display:flex}.k-dialog-box{position:relative;background:#efefef;width:22rem;-webkit-box-shadow:rgba(22,23,26,.2) 0 2px 10px;box-shadow:0 2px 10px rgba(22,23,26,.2);border-radius:1px;line-height:1;max-height:calc(100vh - 3rem);margin:1.5rem;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.k-dialog-box[data-size=small]{width:20rem}.k-dialog-box[data-size=medium]{width:30rem}.k-dialog-box[data-size=large]{width:40rem}.k-dialog-notification{padding:.75rem 1.5rem;background:#16171a;width:100%;line-height:1.25rem;color:#fff;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-negative:0;flex-shrink:0;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.k-dialog-notification[data-theme=error]{background:#d16464;color:#000}.k-dialog-notification[data-theme=success]{background:#a7bd68;color:#000}.k-dialog-notification p{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;word-wrap:break-word;overflow:hidden}.k-dialog-notification .k-button{display:-webkit-box;display:-ms-flexbox;display:flex;margin-left:1rem}.k-dialog-body{padding:1.5rem;overflow-y:auto;overflow-x:hidden}.k-dialog-body .k-fieldset{padding-bottom:.5rem}.k-dialog-footer{border-top:1px solid #ccc;padding:0;border-bottom-left-radius:1px;border-bottom-right-radius:1px;line-height:1;-ms-flex-negative:0;flex-shrink:0}.k-dialog-footer .k-button-group{display:-webkit-box;display:-ms-flexbox;display:flex;margin:0;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.k-dialog-footer .k-button-group .k-button{padding:.75rem 1rem;line-height:1.25rem}.k-dialog-footer .k-button-group .k-button:first-child{text-align:left;padding-left:1.5rem}.k-dialog-footer .k-button-group .k-button:last-child{text-align:right;padding-right:1.5rem}.k-dialog-pagination{margin-bottom:-1.5rem;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align: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:1px}.k-error-details{background:#fff;display:block;overflow:auto;padding:1rem;font-size:.875rem;line-height:1.25em;margin-top:.75rem}.k-error-details dt{color:#d16464;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 #efefef;padding-bottom:.25rem;margin-bottom:.25rem}.k-files-dialog .k-list-item{cursor:pointer}.k-page-remove-warning{margin:1.5rem 0}.k-page-remove-warning .k-box{font-size:1rem;line-height:1.5em;padding-top:.75rem;padding-bottom:.75rem}.k-pages-dialog-navbar{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;margin-bottom:.5rem;padding-right:38px}.k-pages-dialog-navbar .k-button{width:38px}.k-pages-dialog-navbar .k-button[disabled]{opacity:0}.k-pages-dialog-navbar .k-headline{-webkit-box-flex:1;-ms-flex-positive:1;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-calendar-input{padding:.5rem;background:#16171a;color:#efefef;border-radius:1px}.k-calendar-table{table-layout:fixed;width:100%;min-width:15rem;padding-top:.5rem}.k-calendar-input>nav{display:-webkit-box;display:-ms-flexbox;display:flex;direction:ltr}.k-calendar-input>nav .k-button{padding:.5rem}.k-calendar-selects{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack: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:400;font-size:.875rem}.k-calendar-selects .k-select-input:focus-within{color:#81a2be!important}.k-calendar-input th{padding:.5rem 0;color:#999;font-size:.75rem;font-weight:400;text-align:center}.k-calendar-day .k-button{width:2rem;height:2rem;margin:0 auto;color:#fff;line-height:1.75rem;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;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:#fff}.k-calendar-day:hover .k-button{border-color:hsla(0,0%,100%,.25)}.k-calendar-day[aria-current=date] .k-button{color:#81a2be;font-weight:500}.k-calendar-day[aria-selected=date] .k-button{border-color:#a7bd68;color:#a7bd68}.k-calendar-today{text-align:center;padding-top:.5rem}.k-calendar-today .k-button{color:#81a2be;font-size:.75rem;padding:1rem}.k-calendar-today .k-button-text{opacity:1}.k-counter{font-size:.75rem;color:#16171a;font-weight:600}.k-counter[data-invalid]{-webkit-box-shadow:none;box-shadow:none;border:0;color:#c82829}.k-counter-rules{color:#777;font-weight:400}[dir=ltr] .k-counter-rules{padding-left:.5rem}[dir=rtl] .k-counter-rules{padding-right:.5rem}.k-form-submitter{display:none}.k-form-buttons[data-theme=changes]{background:#de935f}.k-form-buttons[data-theme=lock]{background:#d16464}.k-form-buttons[data-theme=unlock]{background:#81a2be}.k-form-buttons .k-view{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.k-form-button.k-button,.k-form-buttons .k-view{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.k-form-button.k-button{font-weight:500;white-space:nowrap;line-height:1;height:2.5rem;padding:0 1rem}.k-form-button:first-child{margin-left:-1rem}.k-form-button:last-child{margin-right:-1rem}.k-form-lock-info{display:-webkit-box;display:-ms-flexbox;display:flex;font-size:.875rem;-webkit-box-align:center;-ms-flex-align:center;align-items:center;line-height:1.5em;padding:.625rem 0;margin-right:3rem}.k-form-lock-info>.k-icon{margin-right:.5rem}.k-form-lock-buttons{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-negative:0;flex-shrink:0}.k-form-lock-loader{-webkit-animation:Spin 4s linear infinite;animation:Spin 4s linear infinite}.k-form-lock-loader .k-icon-loader{display:-webkit-box;display:-ms-flexbox;display:flex}.k-form-indicator-icon{color:#de935f}.k-form-indicator-info{font-size:.875rem;font-weight:600;padding:.75rem 1rem .25rem;line-height:1.25em;width:15rem}.k-field-label{font-weight:600;display:block;padding:0 0 .75rem;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;line-height:1.25rem}.k-field-label abbr{text-decoration:none;color:#999;padding-left:.25rem}.k-field-header{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.k-field-options{position:absolute;top:calc(-.5rem - 1px)}[dir=ltr] .k-field-options{right:0}[dir=rtl] .k-field-options{left:0}.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:-webkit-box;display:-ms-flexbox;display:flex}.k-field[data-disabled]{cursor:not-allowed}.k-field[data-disabled] *{pointer-events:none}.k-field[data-disabled] .k-text[data-theme=help] *{pointer-events:auto}.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:auto}.k-input{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;line-height:1;border:0;outline:0;background:none}.k-input-element{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.k-input-icon{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;line-height:0}.k-input[data-disabled]{pointer-events:none}.k-input[data-theme=field]{line-height:1;border:1px solid #ccc;background:#fff}.k-input[data-theme=field]:focus-within{border:1px solid #4271ae;-webkit-box-shadow:rgba(66,113,174,.25) 0 0 0 2px;box-shadow:0 0 0 2px rgba(66,113,174,.25)}.k-input[data-theme=field][data-disabled]{background:#efefef}.k-input[data-theme=field] .k-input-icon{width:2.25rem}.k-input[data-theme=field] .k-input-after,.k-input[data-theme=field] .k-input-before,.k-input[data-theme=field] .k-input-icon{-ms-flex-item-align:stretch;align-self:stretch;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-negative:0;flex-shrink:0}.k-input[data-theme=field] .k-input-after,.k-input[data-theme=field] .k-input-before{padding:0 .5rem}.k-input[data-theme=field] .k-input-before{color:#777;padding-right:0}.k-input[data-theme=field] .k-input-after{color:#777;padding-left:0}.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:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-ms-flex-negative:0;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:.5rem;line-height:1.25rem}.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:.5rem}[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:.5rem}.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:#4271ae;font-weight:600}.k-input[data-theme=field] .k-time-input .k-time-input-meridiem{padding-left:.5rem}.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}.k-input[data-theme=field][data-type=checkboxes] .k-input-before{border-right:1px solid #efefef}.k-input[data-theme=field][data-type=checkboxes] .k-input-element+.k-input-after,.k-input[data-theme=field][data-type=checkboxes] .k-input-element+.k-input-icon{border-left:1px solid #efefef}.k-input[data-theme=field][data-type=checkboxes] .k-input-element{overflow:hidden}.k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input{display:grid;grid-template-columns:1fr;margin-bottom:-1px;margin-right:-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)}}.k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input li{border-right:1px solid #efefef;border-bottom:1px solid #efefef}.k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input label{display:block;line-height:1.25rem;padding:.5rem .5rem}.k-input[data-theme=field][data-type=checkboxes] .k-checkbox-input-icon{top:.625rem;left:.5rem;margin-top:0}.k-input[data-theme=field][data-type=radio] .k-input-before{border-right:1px solid #efefef}.k-input[data-theme=field][data-type=radio] .k-input-element+.k-input-after,.k-input[data-theme=field][data-type=radio] .k-input-element+.k-input-icon{border-left:1px solid #efefef}.k-input[data-theme=field][data-type=radio] .k-input-element{overflow:hidden}.k-input[data-theme=field][data-type=radio] .k-radio-input{display:grid;grid-template-columns:1fr;margin-bottom:-1px;margin-right:-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)}}.k-input[data-theme=field][data-type=radio] .k-radio-input li{border-right:1px solid #efefef;border-bottom:1px solid #efefef}.k-input[data-theme=field][data-type=radio] .k-radio-input label{display:block;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;min-height:2.25rem;line-height:1.25rem;padding:.5rem .5rem}.k-input[data-theme=field][data-type=radio] .k-radio-input label:before{top:.625rem;left:.5rem;margin-top:-1px}.k-input[data-theme=field][data-type=radio] .k-radio-input .k-radio-input-info{display:block;font-size:.875rem;color:#777;line-height:1.25rem;padding-top:.125rem}.k-input[data-theme=field][data-type=radio] .k-radio-input .k-icon{width:2.25rem;height:2.25rem;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.k-input[data-theme=field][data-type=range] .k-range-input{padding:.5rem}.k-input[data-theme=field][data-type=select]{position:relative}.k-input[data-theme=field][data-type=select] .k-input-icon{position:absolute;top:0;bottom:0}[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=tags] .k-tags-input{padding:.25rem .25rem 0 .25rem}.k-input[data-theme=field][data-type=tags] .k-tag{margin-right:.25rem;margin-bottom:.25rem;height:1.75rem;font-size:.875rem}.k-input[data-theme=field][data-type=tags] .k-tags-input input{font-size:.875rem;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=multiselect]{position:relative}.k-input[data-theme=field][data-type=multiselect] .k-multiselect-input{padding:.25rem 2rem 0 .25rem;min-height:2.25rem}.k-input[data-theme=field][data-type=multiselect] .k-tag{margin-right:.25rem;margin-bottom:.25rem;height:1.75rem;font-size:.875rem}.k-input[data-theme=field][data-type=multiselect] .k-input-icon{position:absolute;top:0;right:0;bottom:0;pointer-events:none}.k-input[data-theme=field][data-type=textarea] .k-textarea-input-native{padding:.25rem .5rem;line-height:1.5rem}.k-input[data-theme=field][data-type=toggle] .k-input-before{padding-right:.25rem}.k-input[data-theme=field][data-type=toggle] .k-toggle-input{padding-left:.5rem}.k-input[data-theme=field][data-type=toggle] .k-toggle-input-label{padding:0 .5rem 0 .75rem;line-height:2.25rem}.k-upload input{position:absolute;top:0}[dir=ltr] .k-upload input{left:-3000px}[dir=rtl] .k-upload input{right:-3000px}.k-upload .k-headline{margin-bottom:.75rem}.k-upload-error-list,.k-upload-list{line-height:1.5em;font-size:.875rem}.k-upload-list-filename{color:#777}.k-upload-error-list li{padding:.75rem;background:#fff;border-radius:1px}.k-upload-error-list li:not(:last-child){margin-bottom:2px}.k-upload-error-filename{color:#c82829;font-weight:600}.k-upload-error-message{color:#777}.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}.k-checkbox-input-label{display:block;padding-left:1.75rem}.k-checkbox-input-icon{position:absolute;left:0;width:1rem;height:1rem;border:2px solid #999}.k-checkbox-input-icon svg{position:absolute;width:12px;height:12px;display:none}.k-checkbox-input-icon path{stroke:#fff}.k-checkbox-input-native:checked+.k-checkbox-input-icon{border-color:#16171a;background:#16171a}.k-checkbox-input-native:checked+.k-checkbox-input-icon svg{display:block}.k-checkbox-input-native:focus+.k-checkbox-input-icon{border-color:#4271ae}.k-checkbox-input-native:focus:checked+.k-checkbox-input-icon{background:#4271ae}.k-date-input{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.k-date-input-separator{padding:0 .125rem}.k-datetime-input{display:-webkit-box;display:-ms-flexbox;display:flex}.k-datetime-input .k-time-input{padding-left:.5rem}.k-text-input{width:100%;border:0;background:none;font:inherit;color:inherit}.k-text-input::-webkit-input-placeholder{color:#999}.k-text-input::-moz-placeholder{color:#999}.k-text-input:-ms-input-placeholder{color:#999}.k-text-input::-ms-input-placeholder{color:#999}.k-text-input::placeholder{color:#999}.k-text-input:focus{outline:0}.k-text-input:invalid{-webkit-box-shadow:none;box-shadow:none;outline:0}.k-multiselect-input{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;position:relative;font-size:.875rem;min-height:2.25rem;line-height:1}.k-multiselect-input .k-sortable-ghost{background:#4271ae}.k-multiselect-input .k-dropdown-content{width:100%}.k-multiselect-search{margin-top:0!important;color:#fff;background:#16171a;border-bottom:1px dashed hsla(0,0%,100%,.2)}.k-multiselect-search>.k-button-text{-webkit-box-flex:1;-ms-flex:1;flex:1}.k-multiselect-search input{width:100%;color:#fff;background:none;border:none;outline:none;padding:.25rem 0;font:inherit}.k-multiselect-options{position:relative;max-height:240px;overflow-y:scroll;padding:.5rem 0}.k-multiselect-option{position:relative}.k-multiselect-option.selected{color:#a7bd68}.k-multiselect-option.disabled:not(.selected) .k-icon{opacity:0}.k-multiselect-option b{color:#81a2be;font-weight:700}.k-multiselect-value{color:#999;margin-left:.25rem}.k-multiselect-value:before{content:" ("}.k-multiselect-value:after{content:")"}.k-multiselect-input[data-layout=list] .k-tag{width:100%;margin-right:0!important}.k-number-input{width:100%;border:0;background:none;font:inherit;color:inherit}.k-number-input::-webkit-input-placeholder{color:$color-light-grey}.k-number-input::-moz-placeholder{color:$color-light-grey}.k-number-input:-ms-input-placeholder{color:$color-light-grey}.k-number-input::-ms-input-placeholder{color:$color-light-grey}.k-number-input::placeholder{color:$color-light-grey}.k-number-input:focus{outline:0}.k-number-input:invalid{-webkit-box-shadow:none;box-shadow:none;outline:0}.k-radio-input li{position:relative;line-height:1.5rem;padding-left:1.75rem}.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;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.k-radio-input label:before{position:absolute;top:.175em;left:0;content:"";width:1rem;height:1rem;border-radius:50%;border:2px solid #999;-webkit-box-shadow:#fff 0 0 0 2px inset;box-shadow:inset 0 0 0 2px #fff}.k-radio-input input:checked+label:before{border-color:#16171a;background:#16171a}.k-radio-input input:focus+label:before{border-color:#4271ae}.k-radio-input input:focus:checked+label:before{background:#4271ae}.k-radio-input-text{display:block}.k-range-input{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;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(8px + var(--ratio)*(100% - 16px));-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;height:16px;background:transparent;font-size:.875rem;line-height:1}.k-range-input-native::-webkit-slider-thumb{-webkit-appearance:none;appearance:none}.k-range-input-native::-webkit-slider-runnable-track{border:none;border-radius:4px;width:100%;height:4px;background:#ccc;background:-webkit-gradient(linear,left top,left bottom,from(#16171a),to(#16171a)) 0/var(--position) 100% no-repeat #ccc;background:linear-gradient(#16171a,#16171a) 0/var(--position) 100% no-repeat #ccc}.k-range-input-native::-moz-range-track{border:none;border-radius:4px;width:100%;height:4px;background:#ccc}.k-range-input-native::-ms-track{border:none;border-radius:4px;width:100%;height:4px;background:#ccc}.k-range-input-native::-moz-range-progress{height:4px;background:#16171a}.k-range-input-native::-ms-fill-lower{height:4px;background:#16171a}.k-range-input-native::-webkit-slider-thumb{margin-top:-6px;-webkit-box-sizing:border-box;box-sizing:border-box;width:16px;height:16px;background:#efefef;border:4px solid #16171a;border-radius:50%;cursor:pointer}.k-range-input-native::-moz-range-thumb{box-sizing:border-box;width:16px;height:16px;background:#efefef;border:4px solid #16171a;border-radius:50%;cursor:pointer}.k-range-input-native::-ms-thumb{margin-top:0;box-sizing:border-box;width:16px;height:16px;background:#efefef;border:4px solid #16171a;border-radius:50%;cursor:pointer}.k-range-input-native::-ms-tooltip{display:none}.k-range-input-native:focus{outline:none}.k-range-input-native:focus::-webkit-slider-runnable-track{border:none;border-radius:4px;width:100%;height:4px;background:#ccc;background:-webkit-gradient(linear,left top,left bottom,from(#4271ae),to(#4271ae)) 0/var(--position) 100% no-repeat #ccc;background:linear-gradient(#4271ae,#4271ae) 0/var(--position) 100% no-repeat #ccc}.k-range-input-native:focus::-moz-range-progress{height:4px;background:#4271ae}.k-range-input-native:focus::-ms-fill-lower{height:4px;background:#4271ae}.k-range-input-native:focus::-webkit-slider-thumb{background:#efefef;border:4px solid #4271ae}.k-range-input-native:focus::-moz-range-thumb{background:#efefef;border:4px solid #4271ae}.k-range-input-native:focus::-ms-thumb{background:#efefef;border:4px solid #4271ae}.k-range-input-tooltip{position:relative;max-width:20%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;color:#fff;font-size:.75rem;line-height:1;text-align:center;border-radius:1px;background:#16171a;margin-left:1rem;padding:0 .25rem;white-space:nowrap}.k-range-input-tooltip:after{position:absolute;top:50%;left:-5px;width:0;height:0;-webkit-transform:translateY(-50%);transform:translateY(-50%);border-top:5px solid transparent;border-right:5px solid #16171a;border-bottom:5px solid transparent;content:""}.k-range-input-tooltip>*{padding:4px}.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}.k-select-input-native[disabled]{cursor:default}.k-select-input-native{font-weight:400}.k-tags-input{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.k-tags-input .k-sortable-ghost{background:#4271ae}.k-tags-input-element{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-ms-flex-preferred-size:0;flex-basis:0;min-width:0}.k-tags-input:focus-within .k-tags-input-element{-ms-flex-preferred-size:4rem;flex-basis:4rem}.k-tags-input-element input{font:inherit;border:0;width:100%;background:none}.k-tags-input-element input:focus{outline:0}.k-tags-input[data-layout=list] .k-tag{width:100%;margin-right:0!important}.k-textarea-input-wrapper{position:relative}.k-textarea-input-native{resize:none;border:0;width:100%;background:none;font:inherit;line-height:1.5em;color:inherit}.k-textarea-input-native::-webkit-input-placeholder{color:#999}.k-textarea-input-native::-moz-placeholder{color:#999}.k-textarea-input-native:-ms-input-placeholder{color:#999}.k-textarea-input-native::-ms-input-placeholder{color:#999}.k-textarea-input-native::placeholder{color:#999}.k-textarea-input-native:focus{outline:0}.k-textarea-input-native:invalid{-webkit-box-shadow:none;box-shadow:none;outline:0}.k-textarea-input-native[data-size=small]{min-height:7.5rem}.k-textarea-input-native[data-size=medium]{min-height:15rem}.k-textarea-input-native[data-size=large]{min-height:30rem}.k-textarea-input-native[data-size=huge]{min-height:45rem}.k-textarea-input-native[data-font=monospace]{font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}.k-toolbar{margin-bottom:.25rem;color:#aaa}.k-textarea-input:focus-within .k-toolbar{position:-webkit-sticky;position:sticky;top:0;right:0;left:0;z-index:1;-webkit-box-shadow:rgba(0,0,0,.05) 0 2px 5px;box-shadow:0 2px 5px rgba(0,0,0,.05);border-bottom:1px solid rgba(0,0,0,.1);color:#000}.k-time-input{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-webkit-box-align:center;-ms-flex-align:center;align-items:center;line-height:1}.k-time-input-separator{padding:0 .125rem}.k-time-input-meridiem{padding-left:.5rem}.k-toggle-input{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.k-toggle-input-native{position:relative;height:16px;width:32px;border-radius:16px;border:2px solid #999;-webkit-box-shadow:inset 0 0 0 2px #fff,inset -16px 0 0 2px #fff;box-shadow:inset 0 0 0 2px #fff,inset -16px 0 0 2px #fff;background-color:#999;outline:0;-webkit-transition:all .1s ease-in-out;transition:all .1s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:pointer;-ms-flex-negative:0;flex-shrink:0}.k-toggle-input-native:checked{border-color:#16171a;-webkit-box-shadow:inset 0 0 0 2px #fff,inset 16px 0 0 2px #fff;box-shadow:inset 0 0 0 2px #fff,inset 16px 0 0 2px #fff;background-color:#16171a}.k-toggle-input-native[disabled]{border-color:#ccc;-webkit-box-shadow:inset 0 0 0 2px #efefef,inset -16px 0 0 2px #efefef;box-shadow:inset 0 0 0 2px #efefef,inset -16px 0 0 2px #efefef;background-color:#ccc}.k-toggle-input-native[disabled]:checked{-webkit-box-shadow:inset 0 0 0 2px #efefef,inset 16px 0 0 2px #efefef;box-shadow:inset 0 0 0 2px #efefef,inset 16px 0 0 2px #efefef}.k-toggle-input-native:focus:checked{border:2px solid #4271ae;background-color:#4271ae}.k-toggle-input-native::-ms-check{opacity:0}.k-toggle-input-label{cursor:pointer;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.k-files-field[data-disabled] *{pointer-events:all!important}body{counter-reset:headline-counter}.k-headline-field{position:relative;padding-top:1.5rem}.k-headline-field[data-numbered]:before{counter-increment:headline-counter;content:counter(headline-counter,decimal-leading-zero);color:#4271ae;font-weight:400;padding-right:.25rem}.k-fieldset>.k-grid .k-column:first-child .k-headline-field{padding-top:0}.k-info-field .k-headline{padding-bottom:.75rem;line-height:1.25rem}.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:#ccc}.k-pages-field[data-disabled] *{pointer-events:all!important}.k-structure-table{position:relative;table-layout:fixed;width:100%;background:#fff;font-size:.875rem;border-spacing:0;-webkit-box-shadow:rgba(22,23,26,.05) 0 2px 5px;box-shadow:0 2px 5px rgba(22,23,26,.05)}.k-structure-table td,.k-structure-table th{border-bottom:1px solid #efefef;line-height:1.25em;overflow:hidden;text-overflow:ellipsis}[dir=ltr] .k-structure-table td,[dir=ltr] .k-structure-table th{border-right:1px solid #efefef}[dir=rtl] .k-structure-table td,[dir=rtl] .k-structure-table th{border-left:1px solid #efefef}.k-structure-table th{position:-webkit-sticky;position:sticky;top:0;right:0;left:0;width:100%;background:#fff;font-weight:400;z-index:1;color:#777;padding:0 .75rem;height:38px}[dir=ltr] .k-structure-table th{text-align:left}[dir=rtl] .k-structure-table th{text-align:right}.k-structure-table td:last-child,.k-structure-table th:last-child{width:38px}[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 tr:last-child td{border-bottom:0}.k-structure-table tbody tr:hover td{background:hsla(0,0%,93.7%,.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=center]{text-align:center}[dir=ltr] .k-structure-table .k-structure-table-column[data-align=right]{text-align:right}[dir=rtl] .k-structure-table .k-structure-table-column[data-align=right]{text-align:left}.k-structure-table .k-structure-table-column[data-align=right]>.k-input{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end}.k-structure-table .k-structure-table-column[data-width="1/2"]{width:50%}.k-structure-table .k-structure-table-column[data-width="1/3"]{width:33.33%}.k-structure-table .k-structure-table-column[data-width="1/4"]{width:25%}.k-structure-table .k-structure-table-column[data-width="1/5"]{width:20%}.k-structure-table .k-structure-table-column[data-width="1/6"]{width:16.66%}.k-structure-table .k-structure-table-column[data-width="1/8"]{width:12.5%}.k-structure-table .k-structure-table-column[data-width="1/9"]{width:11.11%}.k-structure-table .k-structure-table-column[data-width="2/3"]{width:66.66%}.k-structure-table .k-structure-table-column[data-width="3/4"]{width:75%}.k-structure-table .k-structure-table-index{width:38px;text-align:center}.k-structure-table .k-structure-table-index-number{font-size:.75rem;color:#999;padding-top:.15rem}.k-structure-table .k-sort-handle{width:38px;height:38px;display:none}.k-structure-table[data-sortable] tr:hover .k-structure-table-index-number{display:none}.k-structure-table[data-sortable] tr:hover .k-sort-handle{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.k-structure-table .k-structure-table-option{width:38px;text-align:center}.k-structure-table .k-structure-table-option .k-button{width:38px;height:38px}.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:#fff;-webkit-box-shadow:rgba(22,23,26,.25) 0 5px 10px;box-shadow:0 5px 10px rgba(22,23,26,.25);outline:2px solid #4271ae;margin-bottom:2px;cursor:grabbing;cursor:-webkit-grabbing}.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:1px;margin-bottom:1px;-webkit-box-shadow:rgba(22,23,26,.05) 0 0 0 3px;box-shadow:0 0 0 3px rgba(22,23,26,.05);border:1px solid #ccc;background:#efefef}.k-structure-form-fields{padding:1.5rem 1.5rem 2rem}.k-structure-form-buttons{border-top:1px solid #ccc;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;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:-webkit-box;display:-ms-flexbox;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:-webkit-box;display:-ms-flexbox;display:flex}.k-field-counter{display:none}.k-text-field:focus-within .k-field-counter{display:block}.k-users-field[data-disabled] *{pointer-events:all!important}.k-toolbar{background:#fff;border-bottom:1px solid #efefef;height:38px}.k-toolbar-wrapper{position:absolute;top:0;right:0;left:0;max-width:100%}.k-toolbar-buttons{display:-webkit-box;display:-ms-flexbox;display:flex}.k-toolbar-divider{width:1px;background:#efefef}.k-toolbar-button{width:36px;height:36px}.k-toolbar-button:hover{background:hsla(0,0%,93.7%,.5)}.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{height:100%}.k-url-field-preview{padding:0 .75rem}.k-url-field-preview a{color:#4271ae;text-decoration:underline;-webkit-transition:color .3s;transition:color .3s;overflow:hidden;white-space:nowrap;max-width:100%;text-overflow:ellipsis}.k-url-field-preview a:hover{color:#000}.k-pages-field-preview{padding:0 .25rem 0 .75rem;display:-webkit-box;display:-ms-flexbox;display:flex}.k-pages-field-preview li{line-height:0;margin-right:.5rem}.k-pages-field-preview .k-link{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;background:#efefef;-webkit-box-shadow:rgba(22,23,26,.05) 0 2px 5px;box-shadow:0 2px 5px rgba(22,23,26,.05)}.k-pages-field-preview-image{width:1.525rem;height:1.525rem;color:#999!important}.k-pages-field-preview figcaption{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;line-height:1.5em;padding:0 .5rem;border:1px solid #ccc;border-left:0;border-radius:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.k-toggle-field-preview label{padding:0 .25rem 0 .75rem;display:-webkit-box;display:-ms-flexbox;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:0 .25rem 0 .75rem}[dir=rtl] .k-toggle-field-preview .k-toggle-input{padding:0 .75rem 0 .25rem}[data-align=right] .k-toggle-field-preview .k-toggle-input{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}[dir=ltr] [data-align=right] .k-toggle-field-preview .k-toggle-input{padding:0 .75rem 0 .25rem}.k-users-field-preview,[dir=rtl] [data-align=right] .k-toggle-field-preview .k-toggle-input{padding:0 .25rem 0 .75rem}.k-users-field-preview{display:-webkit-box;display:-ms-flexbox;display:flex}.k-users-field-preview li{line-height:0;margin-right:.5rem}.k-users-field-preview .k-link{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;background:#efefef;-webkit-box-shadow:rgba(22,23,26,.05) 0 2px 5px;box-shadow:0 2px 5px rgba(22,23,26,.05)}.k-users-field-preview-avatar{width:1.525rem;height:1.525rem;color:#999!important}.k-users-field-preview-avatar.k-image{display:block}.k-users-field-preview figcaption{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;line-height:1.5em;padding:0 .5rem;border:1px solid #ccc;border-left:0;border-radius:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.k-bar{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;line-height:1}.k-bar-slot{-webkit-box-flex:1;-ms-flex-positive:1;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{background:#d9d9d9;border-radius:1px;padding:.375rem .75rem;line-height:1.25rem;border-left:2px solid #999;padding:.5rem 1.5rem;word-wrap:break-word;font-size:.875rem}.k-box[data-theme=code]{background:#16171a;border:1px solid #000;color:#efefef;font-family:Input,Menlo,monospace;font-size:.875rem;line-height:1.5}.k-box[data-theme=button]{padding:0}.k-box[data-theme=button] .k-button{padding:0 .75rem;height:2.25rem;width:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;line-height:2rem;text-align:left}.k-box[data-theme=positive]{background:#dbe4c1;border:0;border-left:2px solid #a7bd68;padding:.5rem 1.5rem}.k-box[data-theme=negative]{background:#eec6c6;border:0;border-left:2px solid #d16464;padding:.5rem 1.5rem}.k-box[data-theme=notice]{background:#f4dac9;border:0;border-left:2px solid #de935f;padding:.5rem 1.5rem}.k-box[data-theme=info]{background:#d5e0e9;border:0;border-left:2px solid #81a2be;padding:.5rem 1.5rem}.k-box[data-theme=empty]{text-align:center;border-left:0;padding:3rem 1.5rem;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;background:#efefef;border-radius:1px;color:#777;border:1px dashed #ccc}.k-box[data-theme=empty] .k-icon{margin-bottom:.5rem;color:#999}.k-box[data-theme=empty] p{color:#777}.k-card{position:relative;border-radius:1px;-webkit-box-shadow:rgba(22,23,26,.05) 0 2px 5px;box-shadow:0 2px 5px rgba(22,23,26,.05)}.k-card,.k-card a{min-width:0;background:#fff}.k-card:focus-within{-webkit-box-shadow:#4271ae 0 0 0 2px;box-shadow:0 0 0 2px #4271ae}.k-card a:focus{outline:0}.k-card .k-sort-handle{position:absolute;top:.75rem;width:2rem;height:2rem;border-radius:1px;background:#fff;opacity:0;color:#16171a;z-index:1;will-change:opacity;-webkit-transition:opacity .3s;transition:opacity .3s}[dir=ltr] .k-card .k-sort-handle{right:.75rem}[dir=rtl] .k-card .k-sort-handle{left:.75rem}.k-cards:hover .k-sort-handle{opacity:.25}.k-card:hover .k-sort-handle{opacity:1}.k-card.k-sortable-ghost{outline:2px solid #4271ae;border-radius:0}.k-card-icon,.k-card-image{border-top-left-radius:1px;border-top-right-radius:1px;overflow:hidden}.k-card-icon{position:relative;display:block}.k-card-icon .k-icon{position:absolute;top:0;right:0;bottom:0;left:0}.k-card-icon .k-icon-emoji{font-size:3rem}.k-card-icon .k-icon svg{width:3rem;height:3rem}.k-card-content{line-height:1.25rem;border-bottom-left-radius:1px;border-bottom-right-radius:1px;min-height:2.25rem;padding:.5rem .75rem;overflow-wrap:break-word;word-wrap:break-word}.k-card-text{display:block;font-weight:400;text-overflow:ellipsis;font-size:.875rem}.k-card-text[data-noinfo]:after{content:" ";height:1em;width:5rem;display:inline-block}.k-card-info{color:#777;display:block;font-size:.875rem;text-overflow:ellipsis;overflow:hidden}[dir=ltr] .k-card-info{margin-right:4rem}[dir=rtl] .k-card-info{margin-left:4rem}.k-card-options{position:absolute;bottom:0}[dir=ltr] .k-card-options{right:0}[dir=rtl] .k-card-options{left:0}.k-card-options>.k-button{position:relative;float:left;height:2.25rem;padding:0 .75rem;line-height:1}.k-card-options-dropdown{top:2.25rem}.k-cards{display:grid;grid-gap:1.5rem;grid-template-columns:repeat(auto-fit,minmax(12rem,1fr))}@media screen and (min-width:30em){.k-cards[data-size=tiny]{grid-template-columns:repeat(auto-fill,minmax(12rem,1fr))}.k-cards[data-size=small]{grid-template-columns:repeat(auto-fill,minmax(16rem,1fr))}.k-cards[data-size=medium]{grid-template-columns:repeat(auto-fill,minmax(24rem,1fr))}.k-cards[data-size=huge],.k-cards[data-size=large]{grid-template-columns:1fr}}@media screen and (min-width:65em){.k-cards[data-size=large]{grid-template-columns:repeat(auto-fill,minmax(32rem,1fr))}}.k-collection-help{padding:.5rem .75rem}.k-collection-footer{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;margin-right:-.75rem;margin-left:-.75rem}.k-collection-pagination{line-height:1.25rem;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}@media screen and (min-width:65em){.k-column[data-width="1/1"],.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="1/2"],.k-column[data-width="2/4"],.k-column[data-width="3/6"]{grid-column-start:span 6}.k-column[data-width="1/3"],.k-column[data-width="2/6"]{grid-column-start:span 4}.k-column[data-width="2/3"],.k-column[data-width="4/6"]{grid-column-start:span 8}.k-column[data-width="1/4"]{grid-column-start:span 3}.k-column[data-width="1/6"]{grid-column-start:span 2}.k-column[data-width="5/6"]{grid-column-start:span 10}.k-column[data-width="3/4"]{grid-column-start:span 9}}.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]:after{display:block;outline:1px solid #4271ae;-webkit-box-shadow:rgba(66,113,174,.25) 0 0 0 3px;box-shadow:0 0 0 3px rgba(66,113,174,.25)}.k-empty{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;border-radius:1px;color:#777;border:1px dashed #ccc}.k-empty p{font-size:.875rem;color:#777}.k-empty>.k-icon{color:#999}.k-empty[data-layout=cards]{text-align:center;padding:1.5rem;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.k-empty[data-layout=cards] .k-icon{margin-bottom:1rem}.k-empty[data-layout=cards] .k-icon svg{width:2rem;height:2rem}.k-empty[data-layout=list]{min-height:38px}.k-empty[data-layout=list]>.k-icon{width:36px;min-height:36px;border-right:1px solid rgba(0,0,0,.05)}.k-empty[data-layout=list]>p{line-height:1.25rem;padding:.5rem .75rem}.k-file-preview{background:#2d2f36}.k-file-preview-layout{display:grid}@media screen and (max-width:65em){.k-file-preview-layout{padding:0!important}}@media screen and (min-width:30em){.k-file-preview-layout{grid-template-columns:50% auto}}@media screen and (min-width:65em){.k-file-preview-layout{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}}.k-file-preview-layout>*{min-width:0}.k-file-preview-image{position:relative;background:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXR0ZXJuIGlkPSJhIiB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHBhdHRlcm5Vbml0cz0idXNlclNwYWNlT25Vc2UiPjxwYXRoIGZpbGw9InJnYmEoMCwgMCwgMCwgMC4yKSIgZD0iTTAgMGgxMHYxMEgwem0xMCAxMGgxMHYxMEgxMHoiLz48L3BhdHRlcm4+PHJlY3QgZmlsbD0idXJsKCNhKSIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIvPjwvc3ZnPg==")}@media screen and (min-width:65em){.k-file-preview-image{width:33.33%}}@media screen and (min-width:90em){.k-file-preview-image{width:25%}}.k-file-preview-image .k-image span{overflow:hidden;padding-bottom:66.66%}@media screen and (min-width:30em) and (max-width:65em){.k-file-preview-image .k-image span{position:absolute;top:0;left:0;bottom:0;right:0;padding-bottom:0!important}}@media screen and (min-width:65em){.k-file-preview-image .k-image span{padding-bottom:100%}}.k-file-preview-placeholder{display:block;padding-bottom:100%}.k-file-preview-image img{padding:3rem}.k-file-preview-image-link{display:block;outline:0}.k-file-preview-image-link.k-link[data-tabbed]{-webkit-box-shadow:none;box-shadow:none;outline:2px solid #4271ae;outline-offset:-2px}.k-file-preview-icon{position:relative;display:block;padding-bottom:100%;overflow:hidden;color:hsla(0,0%,100%,.5)}.k-file-preview-icon svg{position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%,-50%) scale(4);transform:translate(-50%,-50%) scale(4)}.k-file-preview-details{padding:1.5rem;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}@media screen and (min-width:65em){.k-file-preview-details{padding:3rem}}.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))}@media screen and (min-width:30em){.k-file-preview-details ul{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}.k-file-preview-details h3{font-size:.875rem;font-weight:500;color:#999}.k-file-preview-details p{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:hsla(0,0%,100%,.75);font-size:.875rem}.k-file-preview-details p a{display:block;width:100%;overflow:hidden;text-overflow:ellipsis}.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{border-bottom:1px solid #ccc;margin-bottom:2rem;padding-top:4vh}.k-header .k-headline{min-height:1.25em;margin-bottom:.5rem}.k-header .k-header-buttons{margin-top:-.5rem;height:3.25rem}.k-header .k-headline-editable{cursor:pointer}.k-header .k-headline-editable .k-icon{color:#999;opacity:0;-webkit-transition:opacity .3s;transition:opacity .3s;display:inline-block}[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:hover .k-icon{opacity:1}.k-header-tabs{position:relative;background:#e9e9e9;border-top:1px solid #ccc;border-left:1px solid #ccc;border-right:1px solid #ccc}.k-header-tabs nav{display:-webkit-box;display:-ms-flexbox;display:flex;margin-left:-1px;margin-right:-1px}.k-header-tabs nav,.k-tab-button.k-button{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.k-tab-button.k-button{position:relative;z-index:1;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:.625rem .75rem;font-size:.75rem;text-transform:uppercase;text-align:center;font-weight:500;border-left:1px solid transparent;border-right:1px solid #ccc;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-ms-flex-negative:1;flex-shrink:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;max-width:15rem}@media screen and (min-width:30em){.k-tab-button.k-button{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}}@media screen and (min-width:30em){.k-tab-button.k-button .k-icon{margin-right:.5rem}}.k-tab-button.k-button>.k-button-text{padding-top:.375rem;font-size:10px;overflow:hidden;max-width:10rem;text-overflow:ellipsis}[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}@media screen and (min-width:30em){.k-tab-button.k-button>.k-button-text{font-size:.75rem;padding-top:0}}.k-tab-button:last-child{border-right:1px solid transparent}.k-tab-button[aria-current]{position:relative;background:#efefef;border-right:1px solid #ccc;pointer-events:none}.k-tab-button[aria-current]:first-child{border-left:1px solid #ccc}.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;height:2px;top:-1px;background:#16171a}.k-tab-button[aria-current]:after{left:0;right:0;height:1px;bottom:-1px;background:#efefef}.k-tabs-dropdown{top:100%;right:0}.k-list .k-list-item:not(:last-child){margin-bottom:2px}.k-list-item{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;background:#fff;border-radius:1px;-webkit-box-shadow:rgba(22,23,26,.05) 0 2px 5px;box-shadow:0 2px 5px rgba(22,23,26,.05)}.k-list-item .k-sort-handle{position:absolute;left:-1.5rem;width:1.5rem;height:38px;opacity:0}.k-list:hover .k-sort-handle{opacity:.25}.k-list-item:hover .k-sort-handle{opacity:1}.k-list-item.k-sortable-ghost{position:relative;outline:2px solid #4271ae;z-index:1;-webkit-box-shadow:rgba(22,23,26,.25) 0 5px 10px;box-shadow:0 5px 10px rgba(22,23,26,.25)}.k-list-item.k-sortable-fallback{opacity:.25!important;overflow:hidden}.k-list-item-image{width:38px;height:38px;overflow:hidden;-ms-flex-negative:0;flex-shrink:0;line-height:0}.k-list-item-image .k-image{width:38px;height:38px;-o-object-fit:contain;object-fit:contain}.k-list-item-image .k-icon{width:38px;height:38px}.k-list-item-image .k-icon svg{opacity:.5}.k-list-item-content{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-ms-flex-negative:1;flex-shrink:1;overflow:hidden;outline:none}.k-list-item-content[data-tabbed]{outline:none;-webkit-box-shadow:#4271ae 0 0 0 2px,rgba(66,113,174,.2) 0 0 0 2px;box-shadow:0 0 0 2px #4271ae,0 0 0 2px rgba(66,113,174,.2)}.k-list-item-text{display:-webkit-box;display:-ms-flexbox;display:flex;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline;width:100%;line-height:1.25rem;padding:.5rem .75rem}.k-list-item-text em{font-style:normal;margin-right:1rem;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;font-size:.875rem;color:#16171a}.k-list-item-text em,.k-list-item-text small{min-width:0;overflow:hidden;text-overflow:ellipsis}.k-list-item-text small{color:#999;font-size:.75rem;color:#777;display:none}@media screen and (min-width:30em){.k-list-item-text small{display:block}}.k-list-item-status{height:auto!important}.k-list-item-options{position:relative;-ms-flex-negative:0;flex-shrink:0}.k-list-item-options .k-dropdown-content{top:38px}.k-list-item-options>.k-button{height:38px;padding:0 12px}.k-list-item-options>.k-button>.k-button-icon{height:38px}.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:calc(100vh - 6rem);display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;padding:0 3rem;overflow:auto}.k-view[data-align=center]>*{-ms-flex-preferred-size:22.5rem;flex-basis:22.5rem}.k-headline{font-size:1rem;font-weight:600;line-height:1.5em}.k-headline[data-size=small]{font-size:.875rem}.k-headline[data-size=large]{font-size:1.25rem;font-weight:400}@media screen and (min-width:65em){.k-headline[data-size=large]{font-size:1.5rem}}.k-headline[data-size=huge]{font-size:1.5rem;line-height:1.15em}@media screen and (min-width:65em){.k-headline[data-size=huge]{font-size:1.75rem}}.k-headline[data-theme=negative]{color:#c82829}.k-headline[data-theme=positive]{color:#5d800d}.k-headline abbr{color:#999;padding-left:.25rem;text-decoration:none}.k-icon{position:relative;line-height:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-ms-flex-negative:0;flex-shrink:0}.k-icon svg{width:1rem;height:1rem;-moz-transform:scale(1)}.k-icon svg *{fill:currentColor}.k-icon[data-back=black]{background:#16171a;color:#fff}.k-icon[data-back=white]{background:#fff;color:#16171a}.k-icon[data-back=pattern]{background:#2d2f36 url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXR0ZXJuIGlkPSJhIiB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHBhdHRlcm5Vbml0cz0idXNlclNwYWNlT25Vc2UiPjxwYXRoIGZpbGw9InJnYmEoMCwgMCwgMCwgMC4yKSIgZD0iTTAgMGgxMHYxMEgwem0xMCAxMGgxMHYxMEgxMHoiLz48L3BhdHRlcm4+PHJlY3QgZmlsbD0idXJsKCNhKSIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIvPjwvc3ZnPg==");color:#fff}.k-icon[data-size=medium] svg{width:2rem;height:2rem}.k-icon[data-size=large] svg{width:3rem;height:3rem}.k-icon-emoji{display:block;line-height:1;font-style:normal;font-size:1rem}@media not all,only screen and (-webkit-min-device-pixel-ratio:2),only screen and (min-resolution:2dppx),only screen and (min-resolution:192dpi){.k-icon-emoji{font-size:1.25rem;margin-left:.2rem}}.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}.k-image-error{position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);color:#fff;font-size:.9em}.k-image-error svg *{fill:hsla(0,0%,100%,.3)}.k-image[data-cover] img{-o-object-fit:cover;object-fit:cover}.k-image[data-back=black] span{background:#16171a}.k-image[data-back=white] span{background:#fff;color:#16171a}.k-image[data-back=white] .k-image-error{background:#16171a;color:#fff}.k-image[data-back=pattern] span{background:#2d2f36 url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXR0ZXJuIGlkPSJhIiB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHBhdHRlcm5Vbml0cz0idXNlclNwYWNlT25Vc2UiPjxwYXRoIGZpbGw9InJnYmEoMCwgMCwgMCwgMC4yKSIgZD0iTTAgMGgxMHYxMEgwem0xMCAxMGgxMHYxMEgxMHoiLz48L3BhdHRlcm4+PHJlY3QgZmlsbD0idXJsKCNhKSIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIvPjwvc3ZnPg==")}.k-progress{-webkit-appearance:none;width:100%;height:.5rem;border-radius:5rem}.k-progress::-webkit-progress-bar{border:none;background:#ccc;height:.5rem;border-radius:20px}.k-progress::-webkit-progress-value{border-radius:20px;background:#4271ae;-webkit-transition:width .3s;transition:width .3s}.k-sort-handle{cursor:move;cursor:grab;cursor:-webkit-grab;color:#16171a;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;line-height:0;width:2rem;height:2rem;display:-webkit-box;display:-ms-flexbox;display:flex;will-change:opacity,color;-webkit-transition:opacity .3s;transition:opacity .3s;z-index:1}.k-sort-handle svg{width:1rem}.k-sort-handle:active{cursor:grabbing;cursor:-webkit-grabbing}.k-text{line-height:1.5em}.k-text p{margin-bottom:1.5em}.k-text a{text-decoration:underline}.k-text>:last-child{margin-bottom:0}.k-text[data-align=center]{text-align:center}.k-text[data-align=right]{text-align:right}.k-text[data-size=tiny]{font-size:.75rem}.k-text[data-size=small]{font-size:.875rem}.k-text[data-size=medium]{font-size:1rem}.k-text[data-size=large]{font-size:1.25rem}.k-text[data-theme=help]{font-size:.875rem;color:#777;line-height:1.25rem}button{line-height:inherit;border:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;font-size:1rem;color:currentColor;background:none;cursor:pointer}button::-moz-focus-inner{padding:0;border:0}.k-button{display:inline-block;position:relative;font-size:.875rem;-webkit-transition:color .3s;transition:color .3s}.k-button,.k-button:focus,.k-button:hover{outline:none}.k-button[data-tabbed]{outline:none;-webkit-box-shadow:#4271ae 0 0 0 2px,rgba(66,113,174,.2) 0 0 0 2px;box-shadow:0 0 0 2px #4271ae,0 0 0 2px rgba(66,113,174,.2)}.k-button *{vertical-align:middle}.k-button[data-responsive] .k-button-text{display:none}@media screen and (min-width:30em){.k-button[data-responsive] .k-button-text{display:inline}}.k-button[data-theme=positive]{color:#5d800d}.k-button[data-theme=negative]{color:#c82829}.k-button-icon{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;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]{opacity:.5;cursor:default}.k-button[data-disabled]:focus .k-button-text,.k-button[data-disabled]:hover .k-button-text{opacity:.75}.k-button-group{font-size:0;margin-left:-.75rem;margin-right:-.75rem}.k-button-group>.k-dropdown{height:3rem;display:inline-block}.k-button-group>.k-button,.k-button-group>.k-dropdown>.k-button{padding:1rem .75rem;line-height:1rem}.k-button-group .k-dropdown-content{top:calc(100% + 1px);margin:0 .75rem}.k-dropdown{position:relative}.k-dropdown-content{position:absolute;top:100%;background:#16171a;color:#fff;z-index:700;-webkit-box-shadow:rgba(22,23,26,.2) 0 2px 10px;box-shadow:0 2px 10px rgba(22,23,26,.2);border-radius:1px;text-align:left;margin-bottom:6rem}[dir=ltr] .k-dropdown-content{left:0}[dir=rtl] .k-dropdown-content{right:0}[dir=ltr] .k-dropdown-content[data-align=right]{left:auto;right:0}[dir=rtl] .k-dropdown-content[data-align=right]{left:0;right:auto}.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 hr{position:relative;padding:.5rem 0;border:0}.k-dropdown-content hr:after{position:absolute;top:.5rem;left:1rem;right:1rem;content:"";height:1px;background:currentColor;opacity:.2}.k-dropdown-item{white-space:nowrap;line-height:1;display:-webkit-box;display:-ms-flexbox;display:flex;width:100%;-webkit-box-align:center;-ms-flex-align:center;align-items:center;font-size:.875rem;padding:6px 16px}.k-dropdown-item:focus{outline:none;-webkit-box-shadow:#4271ae 0 0 0 2px,rgba(66,113,174,.2) 0 0 0 2px;box-shadow:0 0 0 2px #4271ae,0 0 0 2px rgba(66,113,174,.2)}.k-dropdown-item .k-button-figure{text-align:center;padding-right:.5rem}.k-link{outline:none}.k-link[data-tabbed]{outline:none;-webkit-box-shadow:#4271ae 0 0 0 2px,rgba(66,113,174,.2) 0 0 0 2px;box-shadow:0 0 0 2px #4271ae,0 0 0 2px rgba(66,113,174,.2)}.k-pagination{-webkit-user-select:none;-moz-user-select:none;-ms-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:.875rem}.k-pagination[data-align=center]{text-align:center}.k-pagination[data-align=right]{text-align:right}.k-dropdown-content.k-pagination-selector{position:absolute;top:100%;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);background:#000}[dir=ltr] .k-dropdown-content.k-pagination-selector{direction:ltr}[dir=rtl] .k-dropdown-content.k-pagination-selector{direction:rtl}.k-pagination-settings{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.k-pagination-settings .k-button{line-height:1}.k-pagination-settings label{display:-webkit-box;display:-ms-flexbox;display:flex;border-right:1px solid hsla(0,0%,100%,.35);-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:.625rem 1rem;font-size:.75rem}.k-pagination-settings label span{margin-right:.5rem}.k-prev-next{direction:ltr}.k-search{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;overflow:auto;background:rgba(22,23,26,.6)}.k-search-box{max-width:30rem;margin:0 auto;-webkit-box-shadow:rgba(22,23,26,.2) 0 2px 10px;box-shadow:0 2px 10px rgba(22,23,26,.2)}@media screen and (min-width:65em){.k-search-box{margin:2.5rem auto}}.k-search-input{background:#efefef}.k-search-input,.k-search-types{display:-webkit-box;display:-ms-flexbox;display:flex}.k-search-types{-ms-flex-negative:0;flex-shrink:0}.k-search-types>.k-button{padding:0 0 0 .7rem;font-size:1rem;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:none;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;font:inherit;padding:.75rem;border:0;height:2.5rem}.k-search-close{width:2.5rem;line-height:1}.k-search input:focus{outline:0}.k-search ul{background:#fff}.k-search li{border-bottom:1px solid #efefef;line-height:1.125;display:-webkit-box;display:-ms-flexbox;display:flex}.k-search li .k-link{display:block;padding:.5rem .75rem;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.k-search li strong{display:block;font-size:.875rem;font-weight:400}.k-search li small{font-size:.75rem;color:#777}.k-search li[data-selected]{outline:2px solid #4271ae;background:rgba(66,113,174,.25);border-bottom:1px solid transparent}.k-search-empty{padding:.825rem .75rem;font-size:.75rem;background:#efefef;border-top:1px dashed #ccc;color:#777}.k-tag{position:relative;font-size:.875rem;line-height:1;cursor:pointer;background-color:#16171a;color:#efefef;border-radius:1px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.k-tag:focus{outline:0;background-color:#4271ae;border-color:#4271ae;color:#fff}.k-tag-text{padding:0 .75rem}.k-tag-toggle{color:hsla(0,0%,100%,.7);width:2rem;height:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;border-left:1px solid hsla(0,0%,100%,.15)}.k-tag-toggle:hover{background:hsla(0,0%,100%,.2);color:#fff}.k-topbar{position:relative;color:#fff;-ms-flex-negative:0;flex-shrink:0;height:2.5rem;line-height:1;background:#16171a}.k-topbar-wrapper{position:relative;margin-left:-.75rem;margin-right:-.75rem}.k-topbar-loader,.k-topbar-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.k-topbar-loader{position:absolute;top:0;right:0;bottom:0;height:2.5rem;width:2.5rem;padding:.75rem;background:#16171a;z-index:1;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.k-topbar-loader svg{height:18px;width:18px;-webkit-animation:Spin .9s linear infinite;animation:Spin .9s linear infinite}.k-topbar-menu{-ms-flex-negative:0;flex-shrink:0}.k-topbar-menu ul{padding:.5rem 0}.k-topbar-menu-button{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.k-topbar-menu-button .k-button-text{opacity:1}.k-topbar-button,.k-topbar-signals-button{padding:.75rem;line-height:1;font-size:.875rem}.k-topbar-signals .k-button .k-button-text{opacity:1}.k-topbar-button .k-button-text{display:-webkit-box;display:-ms-flexbox;display:flex;opacity:1}.k-topbar-view-button{-ms-flex-negative:0;flex-shrink:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}[dir=ltr] .k-topbar-view-button{padding-right:0}[dir=rtl] .k-topbar-view-button{padding-left:0}[dir=ltr] .k-topbar-view-button .k-icon{margin-right:.5rem}[dir=rtl] .k-topbar-view-button .k-icon{margin-left:.5rem}.k-topbar-crumbs{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;display:-webkit-box;display:-ms-flexbox;display:flex}.k-topbar-crumbs a{position:relative;font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:none;padding-top:.75rem;padding-bottom:.75rem;line-height:1;-webkit-transition:opacity .3s;transition:opacity .3s;outline:none}.k-topbar-crumbs a:before{content:"/";padding:0 .5rem;opacity:.25}.k-topbar-crumbs a:focus,.k-topbar-crumbs a:hover{opacity:1}.k-topbar-crumbs a[data-tabbed]{outline:none;-webkit-box-shadow:#4271ae 0 0 0 2px,rgba(66,113,174,.2) 0 0 0 2px;box-shadow:0 0 0 2px #4271ae,0 0 0 2px rgba(66,113,174,.2)}.k-topbar-crumbs a:not(:last-child){max-width:15vw}.k-topbar-breadcrumb-menu{-ms-flex-negative:0;flex-shrink:0}@media screen and (min-width:30em){.k-topbar-crumbs a{display:block}.k-topbar-breadcrumb-menu{display:none}}.k-topbar-signals{position:absolute;top:0;background:#16171a;height:2.5rem;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}[dir=ltr] .k-topbar-signals{right:0}[dir=rtl] .k-topbar-signals{left:0}.k-topbar-signals:before{position:absolute;content:"";top:0;bottom:0;width:.5rem}[dir=ltr] .k-topbar-signals:before{left:-.5rem;background:-webkit-linear-gradient(left,rgba(22,23,26,0),#16171a)}[dir=rtl] .k-topbar-signals:before{right:-.5rem;background:-webkit-linear-gradient(right,rgba(22,23,26,0),#16171a)}.k-topbar-signals .k-button{line-height:1}.k-topbar-notification{font-weight:600;line-height:1;display:-webkit-box;display:-ms-flexbox;display:flex}.k-topbar .k-button[data-theme=positive]{color:#a7bd68}.k-topbar .k-button[data-theme=negative]{color:#d16464}.k-topbar .k-button[data-theme=negative] .k-button-text{display:none}@media screen and (min-width:30em){.k-topbar .k-button[data-theme=negative] .k-button-text{display:inline}}.k-topbar .k-button[data-theme] .k-button-text{opacity:1}.k-topbar .k-dropdown-content{color:#16171a;background:#fff}.k-topbar .k-dropdown-content hr:after{opacity:.1}.k-topbar-menu [aria-current] .k-link{color:#4271ae;font-weight:500}.k-registration{display:inline-block;margin-right:1rem;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.k-registration p{color:#d16464;font-size:.875rem;margin-right:1rem;font-weight:600;display:none}@media screen and (min-width:90em){.k-registration p{display:block}}.k-registration .k-button{color:#fff}.k-section,.k-sections{padding-bottom:3rem}.k-section-header{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline;z-index:1}.k-section-header .k-headline{line-height:1.25rem;padding-bottom:.75rem;min-height:2rem}.k-section-header .k-button-group{position:absolute;top:-.875rem}[dir=ltr] .k-section-header .k-button-group{right:0}[dir=rtl] .k-section-header .k-button-group{left:0}.k-fields-issue-headline,.k-info-section-headline{margin-bottom:.5rem}.k-fields-section input[type=submit]{display:none}[data-locked] .k-fields-section{opacity:.2;pointer-events:none}.k-browser-view .k-error-view-content{text-align:left}.k-error-view{position:absolute;top:0;right:0;bottom:0;left:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.k-error-view-content{line-height:1.5em;max-width:25rem;text-align:center}.k-error-view-icon{color:#c82829;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:.875rem}.k-installation-issues li{position:relative;padding:1.5rem;background:#fff}[dir=ltr] .k-installation-issues li{padding-left:3.5rem}[dir=rtl] .k-installation-issues li{padding-right:3.5rem}.k-installation-issues .k-icon{position:absolute;top:calc(1.5rem + 2px)}[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 svg *{fill:#c82829}.k-installation-issues li:not(:last-child){margin-bottom:2px}.k-installation-issues li code{font:inherit;color:#c82829}.k-installation-view .k-button[type=submit]{padding:1rem}[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-login-form label abbr{visibility:hidden}.k-login-buttons{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:1.5rem 0}.k-login-button{padding:.5rem 1rem;font-weight:500;-webkit-transition:opacity .3s;transition:opacity .3s}[dir=ltr] .k-login-button{margin-right:-1rem}[dir=rtl] .k-login-button{margin-left:-1rem}.k-login-button span{opacity:1}.k-login-button[disabled]{opacity:.25}.k-login-checkbox{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:.5rem 0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;font-size:.875rem;cursor:pointer}.k-login-checkbox .k-checkbox-text{opacity:.75;-webkit-transition:opacity .3s;transition:opacity .3s}.k-login-checkbox:focus span,.k-login-checkbox:hover span{opacity:1}.k-login-alert{padding:.5rem .75rem;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;min-height:38px;margin-bottom:2rem;background:#c82829;color:#fff;font-size:.875rem;border-radius:1px;-webkit-box-shadow:rgba(22,23,26,.2) 0 2px 10px;box-shadow:0 2px 10px rgba(22,23,26,.2);cursor:pointer}.k-status-flag svg{width:14px;height:14px}.k-status-flag-listed .k-icon{color:#a7bd68}.k-status-flag-unlisted .k-icon{color:#81a2be}.k-status-flag-draft .k-icon{color:#d16464}.k-status-flag[disabled]{opacity:1}.k-settings-view section{margin-bottom:3rem}.k-settings-view .k-header{margin-bottom:1.5rem}.k-settings-view header{margin-bottom:.5rem;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.k-settings-view header,.k-system-info-box{display:-webkit-box;display:-ms-flexbox;display:flex}.k-system-info-box{background:#fff;padding:.75rem}.k-system-info-box li{-ms-flex-negative:0;flex-shrink:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-ms-flex-preferred-size:0;flex-basis:0}.k-system-info-box dt{font-size:.875rem;color:#777;margin-bottom:.25rem}.k-system-unregistered{color:#c82829}.k-languages-section{margin-bottom:2rem}.k-user-profile{background:#fff}.k-user-profile>.k-view{padding-top:3rem;padding-bottom:3rem;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;line-height:0}.k-user-profile .k-button-group{overflow:hidden}[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 .k-button{display:block;padding-top:.25rem;padding-bottom:.25rem;overflow:hidden;white-space:nowrap}.k-user-profile .k-button-group .k-button[disabled]{opacity:1}.k-user-profile .k-dropdown-content{margin-top:.5rem;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}.k-user-view-image .k-image{display:block;width:4rem;height:4rem;line-height:0}.k-user-view-image .k-button-text{opacity:1}.k-user-view-image .k-icon{width:4rem;height:4rem;background:#16171a;color:#999}.k-user-name-placeholder{color:#999;-webkit-transition:color .3s;transition:color .3s}.k-header[data-editable] .k-user-name-placeholder:hover{color:#16171a} \ No newline at end of file diff --git a/kirby/panel/dist/favicon.png b/kirby/panel/dist/favicon.png new file mode 100755 index 0000000..ecf0cf8 Binary files /dev/null and b/kirby/panel/dist/favicon.png differ diff --git a/kirby/panel/dist/img/icons.svg b/kirby/panel/dist/img/icons.svg new file mode 100755 index 0000000..090d7ab --- /dev/null +++ b/kirby/panel/dist/img/icons.svg @@ -0,0 +1,424 @@ + diff --git a/kirby/panel/dist/js/app.js b/kirby/panel/dist/js/app.js new file mode 100755 index 0000000..be8e75e --- /dev/null +++ b/kirby/panel/dist/js/app.js @@ -0,0 +1 @@ +(function(t){function e(e){for(var i,o,r=e[0],l=e[1],u=e[2],d=0,p=[];d0?e.$store.dispatch("notification/error",{message:e.$t("error.page.changeStatus.incomplete"),details:n.errors}):void("default"===n.blueprint.num?e.$api.pages.get(t,{select:["siblings"]}).then(function(t){e.setup(Object(I["a"])({},n,{siblings:t.siblings}))}).catch(function(t){e.$store.dispatch("notification/error",t)}):e.setup(Object(I["a"])({},n,{siblings:[]})))}).catch(function(t){e.$store.dispatch("notification/error",t)})},setup:function(t){this.page=t,this.form.position=t.num||t.siblings.length+1,this.form.status=t.status,this.states=t.blueprint.status,this.$refs.dialog.open()},submit:function(){this.$refs.form.submit()},changeStatus:function(){var t=this;this.$api.pages.status(this.page.id,this.form.status,this.form.position||1).then(function(){t.success({message:":)",event:"page.changeStatus"})}).catch(function(e){t.$refs.dialog.error(e.message)})}}},ye=_e,xe=Object(_["a"])(ye,ke,$e,!1,null,null,null),we=xe.exports,Oe=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",attrs:{button:t.$t("change"),size:"medium",theme:"positive"},on:{submit:function(e){return t.$refs.form.submit()}}},[n("k-form",{ref:"form",attrs:{fields:t.fields},on:{submit:t.submit},model:{value:t.page,callback:function(e){t.page=e},expression:"page"}})],1)},Ce=[],Se={mixins:[C],data:function(){return{blueprints:[],page:{id:null,template:null}}},computed:{fields:function(){return{template:{label:this.$t("template"),type:"select",required:!0,empty:!1,options:this.page.blueprints,icon:"template"}}}},methods:{open:function(t){var e=this;this.$api.pages.get(t,{select:["id","template","blueprints"]}).then(function(t){if(t.blueprints.length<=1)return e.$store.dispatch("notification/error",{message:e.$t("error.page.changeTemplate.invalid",{slug:t.id})});e.page=t,e.page.blueprints=e.page.blueprints.map(function(t){return{text:t.title,value:t.name}}),e.$refs.dialog.open()}).catch(function(t){e.$store.dispatch("notification/error",t)})},submit:function(){var t=this;this.$events.$emit("keydown.cmd.s"),this.$api.pages.template(this.page.id,this.page.template).then(function(){t.success({message:":)",event:"page.changeTemplate"})}).catch(function(e){t.$refs.dialog.error(e.message)})}}},Ee=Se,je=Object(_["a"])(Ee,Oe,Ce,!1,null,null,null),Te=je.exports,Ie=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",attrs:{button:t.$t("change"),size:"medium",theme:"positive"},on:{submit:function(e){return t.$refs.form.submit()}}},[n("k-form",{ref:"form",on:{submit:t.submit}},[n("k-text-field",t._b({attrs:{value:t.slug},on:{input:function(e){return t.sluggify(e)}}},"k-text-field",t.field,!1),[n("k-button",{attrs:{slot:"options",icon:"wand","data-options":""},on:{click:function(e){return t.sluggify(t.page.title)}},slot:"options"},[t._v("\n "+t._s(t.$t("page.changeSlug.fromTitle"))+"\n ")])],1)],1)],1)},Le=[],qe={mixins:[C],data:function(){return{slug:null,url:null,page:{id:null,parent:null,title:null}}},computed:{field:function(){return{name:"slug",label:this.$t("slug"),type:"text",required:!0,icon:"url",help:"/"+this.url,counter:!1,preselect:!0}},slugs:function(){return this.$store.state.languages.current?this.$store.state.languages.current.rules:this.system.slugs},system:function(){return this.$store.state.system.info}},methods:{sluggify:function(t){this.slug=this.$helper.slug(t,[this.slugs,this.system.ascii]),this.page.parents?this.url=this.page.parents.map(function(t){return t.slug}).concat([this.slug]).join("/"):this.url=this.slug},open:function(t){var e=this;this.$api.pages.get(t,{view:"panel"}).then(function(t){e.page=t,e.sluggify(e.page.slug),e.$refs.dialog.open()}).catch(function(t){e.$store.dispatch("notification/error",t)})},submit:function(){var t=this;if(this.slug===this.page.slug)return this.$refs.dialog.close(),void this.$store.dispatch("notification/success",":)");0!==this.slug.length?this.$api.pages.slug(this.page.id,this.slug).then(function(e){t.$store.dispatch("content/move",["pages/"+t.page.id,"pages/"+e.id]);var n={message:":)",event:"page.changeSlug"};!t.$route.params.path||t.page.id!==t.$route.params.path.replace(/\+/g,"/")||t.$store.state.languages.current&&!0!==t.$store.state.languages.current.default||(n.route=t.$api.pages.link(e.id),delete n.event),t.success(n)}).catch(function(e){t.$refs.dialog.error(e.message)}):this.$refs.dialog.error(this.$t("error.page.slug.invalid"))}}},Ae=qe,Ne=Object(_["a"])(Ae,Ie,Le,!1,null,null,null),Be=Ne.exports,Pe=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.models.length?[n("k-list",t._l(t.models,function(e){return n("k-list-item",{key:e.id,attrs:{text:e.text,info:e.info,image:e.image,icon:e.icon},on:{click:function(n){return t.toggle(e)}}},[n("template",{slot:"options"},[t.isSelected(e)?n("k-button",{attrs:{slot:"options",autofocus:!0,icon:t.checkedIcon,tooltip:t.$t("remove"),theme:"positive"},slot:"options"}):n("k-button",{attrs:{slot:"options",autofocus:!0,tooltip:t.$t("select"),icon:"circle-outline"},slot:"options"}),t.model?n("k-button",{attrs:{disabled:!e.hasChildren,tooltip:t.$t("open"),icon:"angle-right"},on:{click:function(n){return n.stopPropagation(),t.go(e)}}}):t._e()],1)],2)}),1),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("\n "+t._s(t.$t("dialog.pages.empty"))+"\n ")])]],2)},De=[],Re={mixins:[Ot],data:function(){var t=Ot.data();return Object(I["a"])({},t,{model:{title:null,parent:null},options:Object(I["a"])({},t.options,{parent:null})})},computed:{fetchData:function(){return{parent:this.options.parent}}},methods:{back:function(){this.options.parent=this.model.parent,this.pagination.page=1,this.fetch()},go:function(t){this.options.parent=t.id,this.pagination.page=1,this.fetch()},onFetched:function(t){this.model=t.model}}},Me=Re,ze=(n("ac27"),Object(_["a"])(Me,Pe,De,!1,null,null,null)),Ue=ze.exports,Fe={extends:ve,methods:{open:function(){var t=this;this.$api.site.get({select:["title"]}).then(function(e){t.page=e,t.$refs.dialog.open()}).catch(function(e){t.$store.dispatch("notification/error",e)})},submit:function(){var t=this;this.page.title=this.page.title.trim(),0!==this.page.title.length?this.$api.site.title(this.page.title).then(function(){t.$store.dispatch("system/title",t.page.title),t.success({message:":)",event:"site.changeTitle"})}).catch(function(e){t.$refs.dialog.error(e.message)}):this.$refs.dialog.error(this.$t("error.site.changeTitle.empty"))}}},He=Fe,Ke=Object(_["a"])(He,a,o,!1,null,null,null),Ve=Ke.exports,Ye=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",attrs:{button:t.$t("create"),size:"medium",theme:"positive"},on:{submit:function(e){return t.$refs.form.submit()},close:t.reset}},[n("k-form",{ref:"form",attrs:{fields:t.fields,novalidate:!0},on:{submit:t.create},model:{value:t.user,callback:function(e){t.user=e},expression:"user"}})],1)},We=[],Ge=n("795b"),Je=n.n(Ge),Ze=(n("5df3"),{mixins:[C],data:function(){return{user:this.emptyForm(),languages:[],roles:[]}},computed:{fields:function(){return{name:{label:this.$t("name"),type:"text",icon:"user"},email:{label:this.$t("email"),type:"email",icon:"email",link:!1,required:!0},password:{label:this.$t("password"),type:"password",icon:"key"},language:{label:this.$t("language"),type:"select",icon:"globe",options:this.languages,required:!0,empty:!1},role:{label:this.$t("role"),type:1===this.roles.length?"hidden":"radio",required:!0,options:this.roles}}}},methods:{create:function(){var t=this;this.$api.users.create(this.user).then(function(){t.success({message:":)",event:"user.create"})}).catch(function(e){t.$refs.dialog.error(e.message)})},emptyForm:function(){return{name:"",email:"",password:"",language:this.$store.state.system.info.defaultLanguage||"en",role:this.$user.role.name}},open:function(){var t=this,e=this.$api.roles.options({canBe:"created"}).then(function(e){t.roles=e,"admin"!==t.$user.role.name&&(t.roles=t.roles.filter(function(t){return"admin"!==t.value}))}).catch(function(e){t.$store.dispatch("notification/error",e)}),n=this.$api.translations.options().then(function(e){t.languages=e}).catch(function(e){t.$store.dispatch("notification/error",e)});Je.a.all([e,n]).then(function(){t.$refs.dialog.open()})},reset:function(){this.user=this.emptyForm()}}}),Xe=Ze,Qe=Object(_["a"])(Xe,Ye,We,!1,null,null,null),tn=Qe.exports,en=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",attrs:{button:t.$t("change"),size:"medium",theme:"positive"},on:{submit:function(e){return t.$refs.form.submit()}}},[n("k-form",{ref:"form",attrs:{fields:t.fields},on:{submit:t.submit},model:{value:t.user,callback:function(e){t.user=e},expression:"user"}})],1)},nn=[],sn={mixins:[C],data:function(){return{user:{id:null,email:null}}},computed:{fields:function(){return{email:{label:this.$t("email"),preselect:!0,required:!0,type:"email"}}}},methods:{open:function(t){var e=this;this.$api.users.get(t,{select:["id","email"]}).then(function(t){e.user=t,e.$refs.dialog.open()}).catch(function(t){e.$store.dispatch("notification/error",t)})},submit:function(){var t=this;this.$api.users.changeEmail(this.user.id,this.user.email).then(function(e){t.$store.dispatch("content/revert","users/"+t.user.id),t.$user.id===t.user.id&&t.$store.dispatch("user/email",t.user.email);var n={message:":)",event:"user.changeEmail"};"User"===t.$route.name&&(n.route=t.$api.users.link(e.id)),t.success(n)}).catch(function(e){t.$refs.dialog.error(e.message)})}}},an=sn,on=Object(_["a"])(an,en,nn,!1,null,null,null),rn=on.exports,ln=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",attrs:{button:t.$t("change"),theme:"positive",icon:"check"},on:{submit:function(e){return t.$refs.form.submit()}}},[n("k-form",{ref:"form",attrs:{fields:t.fields},on:{submit:t.submit},model:{value:t.user,callback:function(e){t.user=e},expression:"user"}})],1)},un=[],cn={mixins:[C],data:function(){return{user:{language:"en"},languages:[]}},computed:{fields:function(){return{language:{label:this.$t("language"),type:"select",icon:"globe",options:this.languages,required:!0,empty:!1}}}},created:function(){var t=this;this.$api.translations.options().then(function(e){t.languages=e})},methods:{open:function(t){var e=this;this.$api.users.get(t,{view:"compact"}).then(function(t){e.user=t,e.$refs.dialog.open()}).catch(function(t){e.$store.dispatch("notification/error",t)})},submit:function(){var t=this;this.$api.users.changeLanguage(this.user.id,this.user.language).then(function(e){t.user=e,t.$user.id===t.user.id&&t.$store.dispatch("user/language",t.user.language),t.success({message:":)",event:"user.changeLanguage"})}).catch(function(e){t.$refs.dialog.error(e.message)})}}},dn=cn,pn=Object(_["a"])(dn,ln,un,!1,null,null,null),fn=pn.exports,hn=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",attrs:{button:t.$t("change"),theme:"positive",icon:"check"},on:{submit:function(e){return t.$refs.form.submit()}}},[n("k-form",{ref:"form",attrs:{fields:t.fields},on:{submit:t.submit},model:{value:t.values,callback:function(e){t.values=e},expression:"values"}})],1)},mn=[],gn={mixins:[C],data:function(){return{user:null,values:{password:null,passwordConfirmation:null}}},computed:{fields:function(){return{password:{label:this.$t("user.changePassword.new"),type:"password",icon:"key"},passwordConfirmation:{label:this.$t("user.changePassword.new.confirm"),icon:"key",type:"password"}}}},methods:{open:function(t){var e=this;this.$api.users.get(t).then(function(t){e.user=t,e.$refs.dialog.open()}).catch(function(t){e.$store.dispatch("notification/error",t)})},submit:function(){var t=this;return!this.values.password||this.values.password.length<8?(this.$refs.dialog.error(this.$t("error.user.password.invalid")),!1):this.values.password!==this.values.passwordConfirmation?(this.$refs.dialog.error(this.$t("error.user.password.notSame")),!1):void this.$api.users.changePassword(this.user.id,this.values.password).then(function(){t.success({message:":)",event:"user.changePassword"})}).catch(function(e){t.$refs.dialog.error(e.message)})}}},bn=gn,vn=Object(_["a"])(bn,hn,mn,!1,null,null,null),kn=vn.exports,$n=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",attrs:{button:t.$t("delete"),theme:"negative",icon:"trash"},on:{submit:t.submit}},[n("k-text",{domProps:{innerHTML:t._s(t.$t("user.delete.confirm",{email:t.user.email}))}})],1)},_n=[],yn={mixins:[C],data:function(){return{user:{email:null}}},methods:{open:function(t){var e=this;this.$api.users.get(t).then(function(t){e.user=t,e.$refs.dialog.open()}).catch(function(t){e.$store.dispatch("notification/error",t)})},submit:function(){var t=this;this.$api.users.delete(this.user.id).then(function(){t.$store.dispatch("content/remove","users/"+t.user.id),t.success({message:":)",event:"user.delete"}),"User"===t.$route.name&&t.$router.push("/users")}).catch(function(e){t.$refs.dialog.error(e.message)})}}},xn=yn,wn=Object(_["a"])(xn,$n,_n,!1,null,null,null),On=wn.exports,Cn=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",attrs:{button:t.$t("rename"),size:"medium",theme:"positive"},on:{submit:function(e){return t.$refs.form.submit()}}},[n("k-form",{ref:"form",attrs:{fields:t.fields},on:{submit:t.submit},model:{value:t.user,callback:function(e){t.user=e},expression:"user"}})],1)},Sn=[],En={mixins:[C],data:function(){return{user:{id:null,name:null}}},computed:{fields:function(){return{name:{label:this.$t("name"),type:"text",icon:"user",preselect:!0}}}},methods:{open:function(t){var e=this;this.$api.users.get(t,{select:["id","name"]}).then(function(t){e.user=t,e.$refs.dialog.open()}).catch(function(t){e.$store.dispatch("notification/error",t)})},submit:function(){var t=this;this.user.name=this.user.name.trim(),this.$api.users.changeName(this.user.id,this.user.name).then(function(){t.$user.id===t.user.id&&t.$store.dispatch("user/name",t.user.name),t.success({message:":)",event:"user.changeName"})}).catch(function(e){t.$refs.dialog.error(e.message)})}}},jn=En,Tn=Object(_["a"])(jn,Cn,Sn,!1,null,null,null),In=Tn.exports,Ln=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",attrs:{button:t.$t("user.changeRole"),size:"medium",theme:"positive"},on:{submit:function(e){return t.$refs.form.submit()}}},[n("k-form",{ref:"form",attrs:{fields:t.fields},on:{submit:t.submit},model:{value:t.user,callback:function(e){t.user=e},expression:"user"}})],1)},qn=[],An={mixins:[C],data:function(){return{roles:[],user:{id:null,role:"visitor"}}},computed:{fields:function(){return{role:{label:this.$t("user.changeRole.select"),type:"radio",required:!0,options:this.roles}}}},methods:{open:function(t){var e=this;this.id=t,this.$api.users.get(t).then(function(t){e.$api.roles.options({canBe:"changed"}).then(function(n){e.roles=n,"admin"!==e.$user.role.name&&(e.roles=e.roles.filter(function(t){return"admin"!==t.value})),e.user=t,e.user.role=e.user.role.name,e.$refs.dialog.open()})}).catch(function(t){e.$store.dispatch("notification/error",t)})},submit:function(){var t=this;this.$api.users.changeRole(this.user.id,this.user.role).then(function(){t.$user.id===t.user.id&&t.$store.dispatch("user/load"),t.success({message:":)",event:"user.changeRole"})}).catch(function(e){t.$refs.dialog.error(e.message)})}}},Nn=An,Bn=Object(_["a"])(Nn,Ln,qn,!1,null,null,null),Pn=Bn.exports,Dn=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.models.length?[n("k-list",t._l(t.models,function(e){return n("k-list-item",{key:e.email,attrs:{text:e.username,image:e.image,icon:e.icon},on:{click:function(n){return t.toggle(e)}}},[t.isSelected(e)?n("k-button",{attrs:{slot:"options",autofocus:!0,icon:t.checkedIcon,tooltip:t.$t("remove"),theme:"positive"},slot:"options"}):n("k-button",{attrs:{slot:"options",autofocus:!0,tooltip:t.$t("select"),icon:"circle-outline"},slot:"options"})],1)}),1),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("\n "+t._s(t.$t("dialog.users.empty"))+"\n ")])]],2)},Rn=[],Mn={mixins:[Ot]},zn=Mn,Un=(n("7568"),Object(_["a"])(zn,Dn,Rn,!1,null,null,null)),Fn=Un.exports,Hn=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,i){return n("k-dropdown-item",t._b({key:i,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"])?null:"button"in e&&0!==e.button?null:(e.preventDefault(),t.close(e))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"backspace",void 0,e.key,void 0)?null:(e.preventDefault(),t.close(e))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"delete",[8,46],e.key,["Backspace","Delete","Del"])?null:(e.preventDefault(),t.close(e))}]}},"k-dropdown-item",e,!1),[t._v("\n "+t._s(e.text)+"\n ")])}),1),t._v("\n "+t._s(t.query)+"\n")],2)},Kn=[],Vn=(n("4917"),n("3b2b"),{props:{limit:10,skip:{type:Array,default:function(){return[]}},options:Array,query:String},data:function(){return{matches:[],selected:{text:null}}},methods:{close:function(){this.$refs.dropdown.close()},onSelect:function(t){this.$refs.dropdown.close(),this.$emit("select",t)},search:function(t){var e=this;if(!(t.length<1)){var n=new RegExp(RegExp.escape(t),"ig");this.matches=this.options.filter(function(t){return!!t.text&&(-1===e.skip.indexOf(t.value)&&null!==t.text.match(n))}).slice(0,this.limit),this.$emit("search",t,this.matches),this.$refs.dropdown.open()}}}}),Yn=Vn,Wn=Object(_["a"])(Yn,Hn,Kn,!1,null,null,null),Gn=Wn.exports,Jn=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.prev}}),n("span",{staticClass:"k-calendar-selects"},[n("k-select-input",{attrs:{options:t.months,disabled:t.disabled,required:!0},model:{value:t.month,callback:function(e){t.month=t._n(e)},expression:"month"}}),n("k-select-input",{attrs:{options:t.years,disabled:t.disabled,required:!0},model:{value:t.year,callback:function(e){t.year=t._n(e)},expression:"year"}})],1),n("k-button",{attrs:{icon:"angle-right"},on:{click:t.next}})],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.numberOfWeeks,function(e){return n("tr",{key:"week_"+e},t._l(t.days(e),function(e,i){return n("td",{key:"day_"+i,staticClass:"k-calendar-day",attrs:{"aria-current":!!t.isToday(e)&&"date","aria-selected":!!t.isCurrent(e)&&"date"}},[e?n("k-button",{on:{click:function(n){return t.select(e)}}},[t._v(t._s(e))]):t._e()],1)}),0)}),0),n("tfoot",[n("tr",[n("td",{staticClass:"k-calendar-today",attrs:{colspan:"7"}},[n("k-button",{on:{click:t.selectToday}},[t._v(t._s(t.$t("today")))])],1)])])])])},Zn=[],Xn={props:{value:String,disabled:Boolean},data:function(){var t=this.value?this.$library.dayjs(this.value):this.$library.dayjs();return{day:t.date(),month:t.month(),year:t.year(),today:this.$library.dayjs(),current:t}},computed:{date:function(){return this.$library.dayjs("".concat(this.year,"-").concat(this.month+1,"-").concat(this.day))},numberOfDays:function(){return this.date.daysInMonth()},numberOfWeeks:function(){return Math.ceil((this.numberOfDays+this.firstWeekday-1)/7)},firstWeekday:function(){var t=this.date.clone().startOf("month").day();return t>0?t:7},weekdays:function(){return[this.$t("days.mon"),this.$t("days.tue"),this.$t("days.wed"),this.$t("days.thu"),this.$t("days.fri"),this.$t("days.sat"),this.$t("days.sun")]},monthnames:function(){return[this.$t("months.january"),this.$t("months.february"),this.$t("months.march"),this.$t("months.april"),this.$t("months.may"),this.$t("months.june"),this.$t("months.july"),this.$t("months.august"),this.$t("months.september"),this.$t("months.october"),this.$t("months.november"),this.$t("months.december")]},months:function(){var t=[];return this.monthnames.forEach(function(e,n){t.push({value:n,text:e})}),t},years:function(){for(var t=[],e=this.year-10;e<=this.year+10;e++)t.push({value:e,text:this.$helper.pad(e)});return t}},watch:{value:function(t){var e=this.$library.dayjs(t);this.day=e.date(),this.month=e.month(),this.year=e.year(),this.current=e}},methods:{days:function(t){for(var e=[],n=7*(t-1)+1,i=n;ithis.numberOfDays?e.push(""):e.push(s)}return e},next:function(){var t=this.date.clone().add(1,"month");this.set(t)},isToday:function(t){return this.month===this.today.month()&&this.year===this.today.year()&&t===this.today.date()},isCurrent:function(t){return this.month===this.current.month()&&this.year===this.current.year()&&t===this.current.date()},prev:function(){var t=this.date.clone().subtract(1,"month");this.set(t)},go:function(t,e){"today"===t&&(t=this.today.year(),e=this.today.month()),this.year=t,this.month=e},set:function(t){this.day=t.date(),this.month=t.month(),this.year=t.year()},selectToday:function(){this.set(this.$library.dayjs()),this.select(this.day)},select:function(t){t&&(this.day=t);var e=this.$library.dayjs(new Date(this.year,this.month,this.day,this.current.hour(),this.current.minute()));this.$emit("input",e.toISOString())}}},Qn=Xn,ti=(n("ee15"),Object(_["a"])(Qn,Jn,Zn,!1,null,null,null)),ei=ti.exports,ni=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()])},ii=[],si=(n("c5f6"),{props:{count:Number,min:Number,max:Number,required:{type:Boolean,default:!1}},computed:{valid:function(){return!1===this.required&&0===this.count||(!0!==this.required||0!==this.count)&&(!(this.min&&this.countthis.max))}}}),ai=si,oi=(n("fc0f"),Object(_["a"])(ai,ni,ii,!1,null,null,null)),ri=oi.exports,li=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(e)}}},[t._t("header"),t._t("default",[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)},ui=[],ci={props:{disabled:Boolean,config:Object,fields:{type:[Array,Object],default:function(){return{}}},novalidate:{type:Boolean,default:!1},value:{type:Object,default:function(){return{}}}},data:function(){return{errors:{},listeners:Object(I["a"])({},this.$listeners,{submit:this.onSubmit})}},methods:{focus:function(t){this.$refs.fields&&this.$refs.fields.focus&&this.$refs.fields.focus(t)},onSubmit:function(){this.$emit("submit",this.value)},submit:function(){this.$refs.submitter.click()}}},di=ci,pi=(n("5d33"),Object(_["a"])(di,li,ui,!1,null,null,null)),fi=pi.exports,hi=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("nav",{staticClass:"k-form-buttons",attrs:{"data-theme":t.mode}},["unlock"===t.mode?n("k-view",[n("p",{staticClass:"k-form-lock-info"},[t._v("\n "+t._s(t.$t("lock.isUnlocked"))+"\n ")]),n("span",{staticClass:"k-form-lock-buttons"},[n("k-button",{staticClass:"k-form-button",attrs:{icon:"download"},on:{click:t.onDownload}},[t._v("\n "+t._s(t.$t("download"))+"\n ")]),n("k-button",{staticClass:"k-form-button",attrs:{icon:"check"},on:{click:t.onResolve}},[t._v("\n "+t._s(t.$t("confirm"))+"\n ")])],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.form.lock.email}))}})],1),t.form.lock.unlockable?n("k-button",{staticClass:"k-form-button",attrs:{icon:"unlock"},on:{click:t.setUnlock}},[t._v("\n "+t._s(t.$t("lock.unlock"))+"\n ")]):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,icon:"undo"},on:{click:t.onRevert}},[t._v("\n "+t._s(t.$t("revert"))+"\n ")]),n("k-button",{staticClass:"k-form-button",attrs:{disabled:t.isDisabled,icon:"check"},on:{click:t.onSave}},[t._v("\n "+t._s(t.$t("save"))+"\n ")])],1):t._e()],1)},mi=[],gi=n("75fc"),bi={data:function(){return{supportsLocking:!0}},computed:{api:function(){return{lock:[this.$route.path+"/lock",null,null,!0],unlock:[this.$route.path+"/unlock",null,null,!0]}},hasChanges:function(){return this.$store.getters["content/hasChanges"]()},form:function(){return{lock:this.$store.state.content.status.lock,unlock:this.$store.state.content.status.unlock}},id:function(){return this.$store.state.content.current},isDisabled:function(){return!1===this.$store.state.content.status.enabled},isLocked:function(){return null!==this.form.lock},isUnlocked:function(){return null!==this.form.unlock},mode:function(){return!0===this.isUnlocked?"unlock":!0===this.isLocked?"lock":!0===this.hasChanges?"changes":void 0}},watch:{hasChanges:function(t,e){if(!1===e&&!0===t)return this.$store.dispatch("heartbeat/remove",this.getLock),void this.$store.dispatch("heartbeat/add",[this.setLock,30]);this.id&&!0===e&&!1===t&&this.removeLock()},id:function(){this.id&&!1===this.hasChanges&&this.$store.dispatch("heartbeat/add",[this.getLock,10])}},created:function(){this.$events.$on("keydown.cmd.s",this.onSave)},destroyed:function(){this.$events.$off("keydown.cmd.s",this.onSave)},methods:{getLock:function(){var t,e=this;return(t=this.$api).get.apply(t,Object(gi["a"])(this.api.lock)).then(function(t){if(!1===t.supported)return e.supportsLocking=!1,void e.$store.dispatch("heartbeat/remove",e.getLock);!1===t.locked?(e.isLocked&&e.form.lock.user!==e.$store.state.user.current.id&&e.$events.$emit("model.reload"),e.$store.dispatch("content/lock",null)):e.$store.dispatch("content/lock",t.locked)}).catch(function(){})},setLock:function(){var t,e=this;!0===this.supportsLocking&&(t=this.$api).patch.apply(t,Object(gi["a"])(this.api.lock)).catch(function(t){if("error.lock.notImplemented"===t.key)return e.supportsLocking=!1,e.$store.dispatch("heartbeat/remove",e.setLock),!1;e.$store.dispatch("content/revert",e.id),e.$store.dispatch("heartbeat/remove",e.setLock),e.$store.dispatch("heartbeat/add",[e.getLock,10])})},removeLock:function(){var t,e=this;!0===this.supportsLocking&&(this.$store.dispatch("heartbeat/remove",this.setLock),(t=this.$api).delete.apply(t,Object(gi["a"])(this.api.lock)).then(function(){e.$store.dispatch("content/lock",null),e.$store.dispatch("heartbeat/add",[e.getLock,10])}).catch(function(){}))},setUnlock:function(){var t,e=this;!0===this.supportsLocking&&(this.$store.dispatch("heartbeat/remove",this.setLock),(t=this.$api).patch.apply(t,Object(gi["a"])(this.api.unlock)).then(function(){e.$store.dispatch("content/lock",null),e.$store.dispatch("heartbeat/add",[e.getLock,10])}).catch(function(){}))},removeUnlock:function(){var t,e=this;!0===this.supportsLocking&&(this.$store.dispatch("heartbeat/remove",this.setLock),(t=this.$api).delete.apply(t,Object(gi["a"])(this.api.unlock)).then(function(){e.$store.dispatch("content/unlock",null),e.$store.dispatch("heartbeat/add",[e.getLock,10])}).catch(function(){}))},onDownload:function(){var t=this,e="";kt()(this.form.unlock).forEach(function(n){e+=n+": \n\n"+t.form.unlock[n],e+="\n\n----\n\n"});var n=document.createElement("a");n.setAttribute("href","data:text/plain;charset=utf-8,"+encodeURIComponent(e)),n.setAttribute("download",this.id+".txt"),n.style.display="none",document.body.appendChild(n),n.click(),document.body.removeChild(n)},onResolve:function(){this.$store.dispatch("content/revert"),this.removeUnlock()},onRevert:function(){this.$store.dispatch("content/revert")},onSave:function(t){var e=this;return!!t&&(t.preventDefault&&t.preventDefault(),!1===this.hasChanges||void this.$store.dispatch("content/save").then(function(){e.$events.$emit("model.update"),e.$store.dispatch("notification/success",":)")}).catch(function(t){403!==t.code&&(t.details&&kt()(t.details).length>0?e.$store.dispatch("notification/error",{message:e.$t("error.form.incomplete"),details:t.details}):e.$store.dispatch("notification/error",{message:e.$t("error.form.notSaved"),details:[{label:"Exception: "+t.exception,message:t.message}]}))}))}}},vi=bi,ki=(n("18dd"),Object(_["a"])(vi,hi,mi,!1,null,null,null)),$i=ki.exports,_i=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-topbar-button",on:{click:t.toggle}},[n("k-icon",{staticClass:"k-form-indicator-icon",attrs:{type:"edit"}})],1),n("k-dropdown-content",{ref:"list",attrs:{align:"right"}},[n("p",{staticClass:"k-form-indicator-info"},[t._v("\n "+t._s(t.$t("lock.unsaved"))+":\n ")]),n("hr"),t._l(t.entries,function(e){return n("k-dropdown-item",{key:e.id,attrs:{icon:e.icon},nativeOn:{click:function(n){return n.stopPropagation(),t.go(e.target)}}},[t._v("\n "+t._s(e.label)+"\n ")])})],2)],1):t._e()},yi=[],xi=(n("28a5"),n("f559"),{data:function(){return{isOpen:!1,entries:[]}},computed:{store:function(){return this.$store.state.content.models},models:function(){var t=this,e=kt()(this.store).filter(function(e){return!!t.store[e]}),n=e.map(function(e){return Object(I["a"])({id:e},t.store[e])});return n.filter(function(t){return kt()(t.changes).length>0})},hasChanges:function(){return this.models.length>0}},methods:{go:function(t){if(t.language&&this.$store.state.languages.current.code!==t.language){var e=this.$store.state.languages.all.filter(function(e){return e.code===t.language})[0];this.$store.dispatch("languages/current",e)}this.$router.push(t.link)},load:function(){var t=this,e=this.models.map(function(e){return t.$api.get(e.api,{view:"compact"},null,!0).then(function(n){var i;if(i=!0===e.id.startsWith("pages/")?{icon:"page",label:n.title,target:{link:t.$api.pages.link(n.id)}}:!0===e.id.startsWith("files/")?{icon:"image",label:n.filename,target:{link:n.link}}:!0===e.id.startsWith("users/")?{icon:"user",label:n.email,target:{link:t.$api.users.link(n.id)}}:{icon:"home",label:n.title,target:{link:"/site"}},t.$store.state.languages.current){var s=e.id.split("/").pop();i.label=i.label+" ("+s+")",i.target.language=s}return i}).catch(function(){return t.$store.dispatch("content/remove",e.id),null})});return Je.a.all(e).then(function(e){t.entries=e.filter(function(t){return null!==t}),0===t.entries.length&&t.$store.dispatch("notification/success",t.$t("lock.unsaved.empty"))})},toggle:function(){var t=this;!1===this.$refs.list.isOpen?this.load().then(function(){t.$refs.list&&t.$refs.list.toggle()}):this.$refs.list.toggle()}}}),wi=xi,Oi=(n("9e26"),Object(_["a"])(wi,_i,yi,!1,null,null,null)),Ci=Oi.exports,Si=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},on:{focusin:function(e){return t.$emit("focus",e)},focusout:function(e){return t.$emit("blur",e)}}},[t._t("header",[n("header",{staticClass:"k-field-header"},[t._t("label",[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",[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",[t.help||t.$slots.help?n("footer",{staticClass:"k-field-footer"},[t._t("help",[t.help?n("k-text",{staticClass:"k-field-help",attrs:{theme:"help"},domProps:{innerHTML:t._s(t.help)}}):t._e()])],2):t._e()])],2)},Ei=[],ji={inheritAttrs:!1,props:{counter:[Boolean,Object],disabled:Boolean,endpoints:Object,help:String,input:[String,Number],label:String,name:[String,Number],required:Boolean,type:String},computed:{labelText:function(){return this.label||" "}}},Ti=ji,Ii=(n("a134"),Object(_["a"])(Ti,Si,Ei,!1,null,null,null)),Li=Ii.exports,qi=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,i){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:i,refInFor:!0,tag:"component",attrs:{name:i,novalidate:t.novalidate,disabled:t.disabled||e.disabled},on:{input:function(n){return t.$emit("input",t.value,e,i)},focus:function(n){return t.$emit("focus",n,e,i)},invalid:function(n,s){return t.onInvalid(n,s,e,i)},submit:function(n){return t.$emit("submit",n,e,i)}},model:{value:t.value[i],callback:function(e){t.$set(t.value,i,e)},expression:"value[fieldName]"}},"component",e,!1)):n("k-box",{attrs:{theme:"negative"}},[n("k-text",{attrs:{size:"small"}},[t._v("\n The field type "),n("strong",[t._v('"'+t._s(i)+'"')]),t._v(" does not exist\n ")])],1)],1)],1):t._e()}),1)],1)},Ai=[],Ni={props:{config:Object,disabled:Boolean,fields:{type:[Array,Object],default:function(){return[]}},novalidate:{type:Boolean,default:!1},value:{type:Object,default:function(){return{}}}},data:function(){return{errors:{}}},methods:{focus:function(t){if(t)this.hasField(t)&&"function"===typeof this.$refs[t][0].focus&&this.$refs[t][0].focus();else{var e=kt()(this.$refs)[0];this.focus(e)}},hasFieldType:function(t){return z["a"].options.components["k-"+t+"-field"]},hasField:function(t){return this.$refs[t]&&this.$refs[t][0]},meetsCondition:function(t){var e=this;if(!t.when)return!0;var n=!0;return kt()(t.when).forEach(function(i){var s=e.value[i.toLowerCase()],a=t.when[i];s!==a&&(n=!1)}),n},onInvalid:function(t,e,n,i){this.errors[i]=e,this.$emit("invalid",this.errors)},hasErrors:function(){return kt()(this.errors).length}}},Bi=Ni,Pi=(n("862b"),Object(_["a"])(Bi,qi,Ai,!1,null,null,null)),Di=Pi.exports,Ri=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",[t._v(t._s(t.before))])],2):t._e(),n("span",{staticClass:"k-input-element",on:{click:function(e){return e.stopPropagation(),t.focus(e)}}},[t._t("default",[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",[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",[n("k-icon",{attrs:{type:t.icon}})])],2):t._e()])},Mi=[],zi={inheritAttrs:!1,props:{after:String,before:String,disabled:Boolean,type:String,icon:[String,Boolean],invalid:Boolean,theme:String,novalidate:{type:Boolean,default:!1},value:{type:[String,Boolean,Number,Object,Array],default:null}},data:function(){var t=this;return{isInvalid:this.invalid,listeners:Object(I["a"])({},this.$listeners,{invalid:function(e,n){t.isInvalid=e,t.$emit("invalid",e,n)}})}},computed:{inputProps:function(){return Object(I["a"])({},this.$props,this.$attrs)}},methods:{blur:function(t){t.relatedTarget&&!1===this.$el.contains(t.relatedTarget)&&this.$refs.input.blur&&this.$refs.input.blur()},focus:function(t){if(t&&t.target&&"INPUT"===t.target.tagName)t.target.focus();else if(this.$refs.input&&this.$refs.input.focus)this.$refs.input.focus();else{var e=this.$el.querySelector("input, select, textarea");e&&e.focus()}}}},Ui=zi,Fi=(n("c7c8"),Object(_["a"])(Ui,Ri,Mi,!1,null,null,null)),Hi=Fi.exports,Ki=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",attrs:{size:"medium"}},[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,i){return n("li",{key:"error-"+i},[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,i){return n("li",{key:"file-"+i},[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)],n("template",{slot:"footer"},[t.errors.length>0?[n("k-button-group",[n("k-button",{attrs:{icon:"check"},on:{click:function(e){return t.$refs.dialog.close()}}},[t._v("\n "+t._s(t.$t("confirm"))+"\n ")])],1)]:t._e()],2)],2)],1)},Vi=[],Yi=n("5176"),Wi=n.n(Yi),Gi={props:{url:{type:String},accept:{type:String,default:"*"},attributes:{type:Object},multiple:{type:Boolean,default:!0},max:{type:Number}},data:function(){return{options:this.$props,completed:{},errors:[],files:[],total:0}},methods:{open:function(t){var e=this;this.params(t),setTimeout(function(){e.$refs.input.click()},1)},params:function(t){this.options=Wi()({},this.$props,t)},select:function(t){this.upload(t.target.files)},drop:function(t,e){this.params(e),this.upload(t)},upload:function(t){var e=this;this.$refs.dialog.open(),this.files=Object(gi["a"])(t),this.completed={},this.errors=[],this.hasErrors=!1,this.options.max&&(this.files=this.files.slice(0,this.options.max)),this.total=this.files.length,this.files.forEach(function(t){e.$helper.upload(t,{url:e.options.url,attributes:e.options.attributes,headers:{"X-CSRF":window.panel.csrf},progress:function(t,n,i){e.$refs[n.name]&&e.$refs[n.name][0]&&e.$refs[n.name][0].set(i)},success:function(t,n,i){e.complete(n,i.data)},error:function(t,n,i){e.errors.push({file:n,message:i.message}),e.complete(n,i.data)}})})},complete:function(t,e){var n=this;if(this.completed[t.name]=e,kt()(this.completed).length==this.total){if(this.$refs.input.value="",this.errors.length>0)return this.$forceUpdate(),void this.$emit("error",this.files);setTimeout(function(){n.$refs.dialog.close(),n.$emit("success",n.files,_t()(n.completed))},250)}}}},Ji=Gi,Zi=(n("5aee"),Object(_["a"])(Ji,Ki,Vi,!1,null,null,null)),Xi=Zi.exports,Qi=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:{disabled:t.disabled,id:t.id,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)}})])},ts=[],es=n("b5ae"),ns={inheritAttrs:!1,props:{autofocus:Boolean,disabled:Boolean,id:[Number,String],label:String,required:Boolean,value:Boolean},watch:{value:function(){this.onInvalid()}},mounted:function(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus:function(){this.$refs.input.focus()},onChange:function(t){this.$emit("input",t)},onInvalid:function(){this.$emit("invalid",this.$v.$invalid,this.$v)},select:function(){this.focus()}},validations:function(){return{value:{required:!this.required||es["required"]}}}},is=ns,ss=(n("42e4"),Object(_["a"])(is,Qi,ts,!1,null,null,null)),as=ss.exports,os=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,i){return n("li",{key:i},[n("k-checkbox-input",{attrs:{id:t.id+"-"+i,label:e.text,value:-1!==t.selected.indexOf(e.value)},on:{input:function(n){return t.onInput(e.value,n)}}})],1)}),0)},rs=[],ls={inheritAttrs:!1,props:{autofocus:Boolean,columns:Number,disabled:Boolean,id:{type:[Number,String],default:function(){return this._uid}},max:Number,min:Number,options:Array,required:Boolean,value:{type:[Array,Object],default:function(){return[]}}},data:function(){return{selected:this.valueToArray(this.value)}},watch:{value:function(t){this.selected=this.valueToArray(t)},selected:function(){this.onInvalid()}},mounted:function(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus:function(){this.$el.querySelector("input").focus()},onInput:function(t,e){if(!0===e)this.selected.push(t);else{var n=this.selected.indexOf(t);-1!==n&&this.selected.splice(n,1)}this.$emit("input",this.selected)},onInvalid:function(){this.$emit("invalid",this.$v.$invalid,this.$v)},select:function(){this.focus()},valueToArray:function(t){return!0===xt()(t)?t:"string"===typeof t?String(t).split(","):"object"===Object(Ht["a"])(t)?_t()(t):void 0}},validations:function(){return{selected:{required:!this.required||es["required"],min:!this.min||Object(es["minLength"])(this.min),max:!this.max||Object(es["maxLength"])(this.max)}}}},us=ls,cs=Object(_["a"])(us,os,rs,!1,null,null,null),ds=cs.exports,ps=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-date-input"},[n("k-select-input",{ref:"years",attrs:{"aria-label":t.$t("year"),options:t.years,disabled:t.disabled,required:t.required,value:t.year,placeholder:"––––"},on:{input:t.setYear,invalid:t.onInvalid}}),n("span",{staticClass:"k-date-input-separator"},[t._v("-")]),n("k-select-input",{ref:"months",attrs:{"aria-label":t.$t("month"),options:t.months,disabled:t.disabled,required:t.required,value:t.month,placeholder:"––"},on:{input:t.setMonth,invalid:t.onInvalid}}),n("span",{staticClass:"k-date-input-separator"},[t._v("-")]),n("k-select-input",{ref:"days",attrs:{"aria-label":t.$t("day"),autofocus:t.autofocus,id:t.id,options:t.days,disabled:t.disabled,required:t.required,value:t.day,placeholder:"––"},on:{input:t.setDay,invalid:t.onInvalid}})],1)},fs=[],hs=n("e814"),ms=n.n(hs),gs={inheritAttrs:!1,props:{autofocus:Boolean,disabled:Boolean,id:[String,Number],max:String,min:String,required:Boolean,value:String},data:function(){return{date:this.$library.dayjs(this.value),minDate:this.calculate(this.min,"min"),maxDate:this.calculate(this.max,"max")}},computed:{day:function(){return isNaN(this.date.date())?"":this.date.date()},days:function(){return this.options(1,this.date.daysInMonth()||31,"days")},month:function(){return isNaN(this.date.date())?"":this.date.month()+1},months:function(){return this.options(1,12,"months")},year:function(){return isNaN(this.date.year())?"":this.date.year()},years:function(){var t=this.date.isBefore(this.minDate)?this.date.year():this.minDate.year(),e=this.date.isAfter(this.maxDate)?this.date.year():this.maxDate.year();return this.options(t,e)}},watch:{value:function(t){this.date=this.$library.dayjs(t)}},methods:{calculate:function(t,e){var n={min:{run:"subtract",take:"startOf"},max:{run:"add",take:"endOf"}}[e],i=t?this.$library.dayjs(t):null;return i&&!1!==i.isValid()||(i=this.$library.dayjs()[n.run](10,"year")[n.take]("year")),i},focus:function(){this.$refs.years.focus()},onInput:function(){!1!==this.date.isValid()?this.$emit("input",this.date.toISOString()):this.$emit("input","")},onInvalid:function(t,e){this.$emit("invalid",t,e)},options:function(t,e){for(var n=[],i=t;i<=e;i++)n.push({value:i,text:this.$helper.pad(i)});return n},set:function(t,e){if(""===e||null===e||!1===e||-1===e)return this.setInvalid(),void this.onInput();if(!1===this.date.isValid())return this.setInitialDate(t,e),void this.onInput();var n=this.date,i=this.date.date();this.date=this.date.set(t,ms()(e)),"month"===t&&this.date.date()!==i&&(this.date=n.set("date",1).set("month",e).endOf("month")),this.onInput()},setInvalid:function(){this.date=this.$library.dayjs("invalid")},setInitialDate:function(t,e){var n=this.$library.dayjs();return this.date=this.$library.dayjs().set(t,ms()(e)),"date"===t&&n.month()!==this.date.month()&&(this.date=n.endOf("month")),this.date},setDay:function(t){this.set("date",t)},setMonth:function(t){this.set("month",t-1)},setYear:function(t){this.set("year",t)}}},bs=gs,vs=(n("6ab3"),Object(_["a"])(bs,ps,fs,!1,null,null,null)),ks=vs.exports,$s=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-datetime-input"},[n("k-date-input",{ref:"dateInput",attrs:{autofocus:t.autofocus,required:t.required,id:t.id,min:t.min,max:t.max,disabled:t.disabled,value:t.dateValue},on:{input:t.setDate}}),n("k-time-input",t._b({ref:"timeInput",attrs:{required:t.required,disabled:t.disabled,value:t.timeValue},on:{input:t.setTime}},"k-time-input",t.timeOptions,!1))],1)},_s=[],ys={inheritAttrs:!1,props:Object(I["a"])({},ks.props,{time:{type:[Boolean,Object],default:function(){return{}}},value:String}),data:function(){return{dateValue:this.parseDate(this.value),timeValue:this.parseTime(this.value),timeOptions:this.setTimeOptions()}},watch:{value:function(t){this.dateValue=this.parseDate(t),this.timeValue=this.parseTime(t),this.onInvalid()}},mounted:function(){this.onInvalid()},methods:{focus:function(){this.$refs.dateInput.focus()},onInput:function(){if(this.timeValue&&this.dateValue){var t=this.dateValue+"T"+this.timeValue+":00";this.$emit("input",t)}else this.$emit("input","")},onInvalid:function(){this.$emit("invalid",this.$v.$invalid,this.$v)},parseDate:function(t){var e=this.$library.dayjs(t);return e.isValid()?e.format("YYYY-MM-DD"):null},parseTime:function(t){var e=this.$library.dayjs(t);return e.isValid()?e.format("HH:mm"):null},setDate:function(t){t&&!this.timeValue&&(this.timeValue=this.$library.dayjs().format("HH:mm")),t?this.dateValue=this.parseDate(t):(this.dateValue=null,this.timeValue=null),this.onInput()},setTime:function(t){t&&!this.dateValue&&(this.dateValue=this.$library.dayjs().format("YYYY-MM-DD")),t?this.timeValue=t:(this.dateValue=null,this.timeValue=null),this.onInput()},setTimeOptions:function(){return!0===this.time?{}:this.time}},validations:function(){return{value:{required:!this.required||es["required"]}}}},xs=ys,ws=(n("4433"),Object(_["a"])(xs,$s,_s,!1,null,null,null)),Os=ws.exports,Cs=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("input",t._g(t._b({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))},Ss=[],Es={inheritAttrs:!1,class:"k-text-input",props:{autocomplete:{type:[Boolean,String],default:"off"},autofocus:Boolean,disabled:Boolean,id:[Number,String],maxlength:Number,minlength:Number,name:[Number,String],pattern:String,placeholder:String,preselect:Boolean,required:Boolean,spellcheck:{type:[Boolean,String],default:"off"},type:{type:String,default:"text"},value:String},data:function(){var t=this;return{listeners:Object(I["a"])({},this.$listeners,{input:function(e){return t.onInput(e.target.value)}})}},watch:{value:function(){this.onInvalid()}},mounted:function(){this.onInvalid(),this.$props.autofocus&&this.focus(),this.$props.preselect&&this.select()},methods:{focus:function(){this.$refs.input.focus()},onInput:function(t){this.$emit("input",t)},onInvalid:function(){this.$emit("invalid",this.$v.$invalid,this.$v)},select:function(){this.$refs.input.select()}},validations:function(){var t=this,e=function(e){return!t.required&&!e||!t.$refs.input.validity.patternMismatch};return{value:{required:!this.required||es["required"],minLength:!this.minlength||Object(es["minLength"])(this.minlength),maxLength:!this.maxlength||Object(es["maxLength"])(this.maxlength),email:"email"!==this.type||es["email"],url:"url"!==this.type||es["url"],pattern:!this.pattern||e}}}},js=Es,Ts=(n("cb8f"),Object(_["a"])(js,Cs,Ss,!1,null,null,null)),Is=Ts.exports,Ls={extends:Is,props:Object(I["a"])({},Is.props,{autocomplete:{type:String,default:"email"},placeholder:{type:String,default:function(){return this.$t("email.placeholder")}},type:{type:String,default:"email"}})},qs=Ls,As=Object(_["a"])(qs,r,l,!1,null,null,null),Ns=As.exports,Bs=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(e)}}},[t._l(t.sorted,function(e){return n("k-tag",{key:e.value,ref:e.value,refInFor:!0,attrs:{removable:!0},on:{remove:function(n){return t.remove(e)}},nativeOn:{click:function(t){t.stopPropagation()},keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"left",37,e.key,["Left","ArrowLeft"])?null:"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"])?null:"button"in e&&2!==e.button?null:t.navigate("next")},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"down",40,e.key,["Down","ArrowDown"])?null:t.$refs.dropdown.open(e)}]}},[t._v("\n "+t._s(e.text)+"\n ")])}),n("k-dropdown-content",{ref:"dropdown",attrs:{slot:"footer"},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(e))}},slot:"footer"},[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",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(e))},input:function(e){e.target.composing||(t.q=e.target.value)}}})]):t._e(),n("div",{staticClass:"k-multiselect-options"},t._l(t.filtered,function(e){return n("k-dropdown-item",{key:e.value,class:{"k-multiselect-option":!0,selected:t.isSelected(e),disabled:!t.addable},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)}})])}),1)],1)],2)},Ps=[],Ds=(n("20d6"),n("55dd"),{inheritAttrs:!1,props:{disabled:Boolean,id:[Number,String],max:Number,min:Number,layout:String,options:{type:Array,default:function(){return[]}},required:Boolean,search:Boolean,separator:{type:String,default:","},sort:Boolean,value:{type:Array,required:!0,default:function(){return[]}}},data:function(){return{state:this.value,q:null,scrollTop:0}},computed:{addable:function(){return!this.max||this.state.length1&&!this.sort},dragOptions:function(){return{disabled:!this.draggable,draggable:".k-tag",delay:1}},filtered:function(){if(null===this.q)return this.options.map(function(t){return Object(I["a"])({},t,{display:t.text,info:t.value})});var t=new RegExp("(".concat(RegExp.escape(this.q),")"),"ig");return this.options.filter(function(e){return String(e.text).match(t)||String(e.value).match(t)}).map(function(e){return Object(I["a"])({},e,{display:String(e.text).replace(t,"$1"),info:String(e.value).replace(t,"$1")})})},sorted:function(){var t=this;if(!1===this.sort)return this.state;var e=this.state,n=function(e){return t.options.findIndex(function(t){return t.value===e.value})};return e.sort(function(t,e){return n(t)-n(e)})}},watch:{value:function(t){this.state=t,this.onInvalid()}},mounted:function(){this.onInvalid(),this.$events.$on("click",this.close),this.$events.$on("keydown.cmd.s",this.close)},destroyed:function(){this.$events.$off("click",this.close),this.$events.$off("keydown.cmd.s",this.close)},methods:{add:function(t){!0===this.addable&&(this.state.push(t),this.onInput())},blur:function(){this.close()},close:function(){!0===this.$refs.dropdown.isOpen&&this.$refs.dropdown.close()},escape:function(){this.q?this.q=null:this.close()},focus:function(){this.$refs.dropdown.open()},index:function(t){return this.state.findIndex(function(e){return e.value===t.value})},isSelected:function(t){return-1!==this.index(t)},navigate:function(t){var e=document.activeElement;switch(t){case"prev":e&&e.previousSibling&&e.previousSibling.focus&&e.previousSibling.focus();break;case"next":e&&e.nextSibling&&e.nextSibling.focus&&e.nextSibling.focus();break}},onClose:function(){!1===this.$refs.dropdown.isOpen&&(document.activeElement===this.$parent.$el&&(this.q=null),this.$parent.$el.focus())},onInput:function(){this.$emit("input",this.sorted)},onInvalid:function(){this.$emit("invalid",this.$v.$invalid,this.$v)},onOpen:function(){var t=this;this.$nextTick(function(){t.$refs.search&&t.$refs.search.focus&&t.$refs.search.focus(),t.$refs.dropdown.$el.querySelector(".k-multiselect-options").scrollTop=t.scrollTop})},remove:function(t){this.state.splice(this.index(t),1),this.onInput()},select:function(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)}},validations:function(){return{state:{required:!this.required||es["required"],minLength:!this.min||Object(es["minLength"])(this.min),maxLength:!this.max||Object(es["maxLength"])(this.max)}}}}),Rs=Ds,Ms=(n("11ae"),Object(_["a"])(Rs,Bs,Ps,!1,null,null,null)),zs=Ms.exports,Us=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("input",t._g(t._b({ref:"input",staticClass:"k-number-input",attrs:{type:"number"},domProps:{value:t.number},on:{keydown:function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"cmd",void 0,e.key,void 0)&&t._k(e.keyCode,"s",void 0,e.key,void 0)?null:t.clean(e)}}},"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,step:t.step},!1),t.listeners))},Fs=[],Hs=n("3be2"),Ks=n.n(Hs),Vs=n("59ad"),Ys=n.n(Vs),Ws=(n("6b54"),{inheritAttrs:!1,props:{autofocus:Boolean,disabled:Boolean,id:[Number,String],max:Number,min:Number,name:[Number,String],placeholder:String,preselect:Boolean,required:Boolean,step:Number,value:{type:[Number,String],default:null}},data:function(){var t=this;return{number:this.format(this.value),timeout:null,listeners:Object(I["a"])({},this.$listeners,{input:function(e){return t.onInput(e.target.value)},blur:this.onBlur})}},watch:{value:function(t){this.number=t},number:{immediate:!0,handler:function(){this.onInvalid()}}},mounted:function(){this.$props.autofocus&&this.focus(),this.$props.preselect&&this.select()},methods:{decimals:function(){var t=Number(this.step||0);return Math.floor(t)===t?0:t.toString().split(".")[1].length||0},format:function(t){if(isNaN(t)||""===t)return"";var e=this.decimals();return t=e?Ys()(t).toFixed(e):Ks()(this.step)?ms()(t):Ys()(t),t},clean:function(){this.number=this.format(this.number)},emit:function(t){t=Ys()(t),isNaN(t)&&(t=""),t!==this.value&&this.$emit("input",t)},focus:function(){this.$refs.input.focus()},onInvalid:function(){this.$emit("invalid",this.$v.$invalid,this.$v)},onInput:function(t){this.number=t,this.emit(t)},onBlur:function(){this.clean(),this.emit(this.number)},select:function(){this.$refs.input.select()}},validations:function(){return{value:{required:!this.required||es["required"],min:!this.min||Object(es["minValue"])(this.min),max:!this.max||Object(es["maxValue"])(this.max)}}}}),Gs=Ws,Js=(n("6018"),Object(_["a"])(Gs,Us,Fs,!1,null,null,null)),Zs=Js.exports,Xs={extends:Is,props:Object(I["a"])({},Is.props,{autocomplete:{type:String,default:"new-password"},type:{type:String,default:"password"}})},Qs=Xs,ta=Object(_["a"])(Qs,u,c,!1,null,null,null),ea=ta.exports,na=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,i){return n("li",{key:i},[n("input",{staticClass:"k-radio-input-native",attrs:{id:t.id+"-"+i,name:t.id,type:"radio"},domProps:{value:e.value,checked:t.value===e.value},on:{change:function(n){return t.onInput(e.value)}}}),n("label",{attrs:{for:t.id+"-"+i}},[e.info?[n("span",{staticClass:"k-radio-input-text"},[t._v(t._s(e.text))]),n("span",{staticClass:"k-radio-input-info"},[t._v(t._s(e.info))])]:[t._v("\n "+t._s(e.text)+"\n ")]],2),e.icon?n("k-icon",{attrs:{type:e.icon}}):t._e()],1)}),0)},ia=[],sa={inheritAttrs:!1,props:{autofocus:Boolean,columns:Number,disabled:Boolean,id:{type:[Number,String],default:function(){return this._uid}},options:Array,required:Boolean,value:[String,Number,Boolean]},watch:{value:function(){this.onInvalid()}},mounted:function(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus:function(){this.$el.querySelector("input").focus()},onInput:function(t){this.$emit("input",t)},onInvalid:function(){this.$emit("invalid",this.$v.$invalid,this.$v)},select:function(){this.focus()}},validations:function(){return{value:{required:!this.required||es["required"]}}}},aa=sa,oa=(n("893d"),Object(_["a"])(aa,na,ia,!1,null,null,null)),ra=oa.exports,la=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()])},ua=[],ca={inheritAttrs:!1,props:{autofocus:Boolean,disabled:Boolean,id:[String,Number],default:[Number,String],max:{type:Number,default:100},min:{type:Number,default:0},name:[String,Number],required:Boolean,step:{type:Number,default:1},tooltip:{type:[Boolean,Object],default:function(){return{before:null,after:null}}},value:[Number,String]},data:function(){var t=this;return{listeners:Object(I["a"])({},this.$listeners,{input:function(e){return t.onInput(e.target.value)}})}},computed:{baseline:function(){return this.min<0?0:this.min},label:function(){return this.required||this.value?this.format(this.position):"–"},position:function(){return this.value||this.default||this.baseline}},watch:{position:function(){this.onInvalid()}},mounted:function(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus:function(){this.$refs.input.focus()},format:function(t){var e=document.lang?document.lang.replace("_","-"):"en",n=this.step.toString().split("."),i=n.length>1?n[1].length:0;return new Intl.NumberFormat(e,{minimumFractionDigits:i}).format(t)},onInvalid:function(){this.$emit("invalid",this.$v.$invalid,this.$v)},onInput:function(t){this.$emit("input",t)}},validations:function(){return{position:{required:!this.required||es["required"],min:!this.min||Object(es["minValue"])(this.min),max:!this.max||Object(es["maxValue"])(this.max)}}}},da=ca,pa=(n("b5d2"),Object(_["a"])(da,la,ua,!1,null,null,null)),fa=pa.exports,ha=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:{autofocus:t.autofocus,"aria-label":t.ariaLabel,disabled:t.disabled,id:t.id,name:t.name,required:t.required},domProps:{value:t.selected}},t.listeners),[t.hasEmptyOption?n("option",{attrs:{disabled:t.required,value:""}},[t._v("\n "+t._s(t.emptyOption)+"\n ")]):t._e(),t._l(t.options,function(e){return n("option",{key:e.value,attrs:{disabled:e.disabled},domProps:{value:e.value}},[t._v("\n "+t._s(e.text)+"\n ")])})],2),t._v("\n "+t._s(t.label)+"\n")])},ma=[],ga={inheritAttrs:!1,props:{autofocus:Boolean,ariaLabel:String,default:String,disabled:Boolean,empty:{type:[Boolean,String],default:!0},id:[Number,String],name:[Number,String],placeholder:String,options:{type:Array,default:function(){return[]}},required:Boolean,value:{type:[String,Number,Boolean],default:""}},data:function(){var t=this;return{selected:this.value,listeners:Object(I["a"])({},this.$listeners,{click:function(e){return t.onClick(e)},change:function(e){return t.onInput(e.target.value)},input:function(t){}})}},computed:{emptyOption:function(){return this.placeholder||"—"},hasEmptyOption:function(){return!1!==this.empty&&!(this.required&&this.default)},label:function(){var t=this.text(this.selected);return""===this.selected||null===this.selected||null===t?this.emptyOption:t}},watch:{value:function(t){this.selected=t,this.onInvalid()}},mounted:function(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus:function(){this.$refs.input.focus()},onClick:function(t){t.stopPropagation(),this.$emit("click",t)},onInvalid:function(){this.$emit("invalid",this.$v.$invalid,this.$v)},onInput:function(t){this.selected=t,this.$emit("input",this.selected)},select:function(){this.focus()},text:function(t){var e=null;return this.options.forEach(function(n){n.value==t&&(e=n.text)}),e}},validations:function(){return{selected:{required:!this.required||es["required"]}}}},ba=ga,va=(n("6a18"),Object(_["a"])(ba,ha,ma,!1,null,null,null)),ka=va.exports,$a=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-draggable",{ref:"box",staticClass:"k-tags-input",attrs:{list:t.tags,"data-layout":t.layout,options:t.dragOptions},on:{end:t.onInput}},[t._l(t.tags,function(e,i){return n("k-tag",{key:i,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"])?null:"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"])?null:"button"in e&&2!==e.button?null:t.navigate("next")}],dblclick:function(n){return t.edit(e)}}},[t._v("\n "+t._s(e.text)+"\n ")])}),n("span",{staticClass:"k-tags-input-element",attrs:{slot:"footer"},slot:"footer"},[n("k-autocomplete",{ref:"autocomplete",attrs:{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:{autofocus:t.autofocus,disabled:t.disabled||t.max&&t.tags.length>=t.max,id:t.id,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(e):null},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"left",37,e.key,["Left","ArrowLeft"])?null:"button"in e&&0!==e.button?null:e.ctrlKey||e.shiftKey||e.altKey||e.metaKey?null:t.leaveInput(e)},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:e.ctrlKey||e.shiftKey||e.altKey||e.metaKey?null:t.enter(e)},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"tab",9,e.key,"Tab")?null:e.ctrlKey||e.shiftKey||e.altKey||e.metaKey?null:t.tab(e)},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"backspace",void 0,e.key,void 0)?null:e.ctrlKey||e.shiftKey||e.altKey||e.metaKey?null:t.leaveInput(e)}]}})])],1)],2)},_a=[],ya={inheritAttrs:!1,props:{autofocus:Boolean,accept:{type:String,default:"all"},disabled:Boolean,icon:{type:[String,Boolean],default:"tag"},id:[Number,String],layout:String,max:Number,min:Number,name:[Number,String],options:{type:Array,default:function(){return[]}},required:Boolean,separator:{type:String,default:","},value:{type:Array,default:function(){return[]}}},data:function(){return{tags:this.prepareTags(this.value),selected:null,newTag:null,tagOptions:this.options.map(function(t){return t.icon="tag",t})}},computed:{dragOptions:function(){return{delay:1,disabled:!this.draggable,draggable:".k-tag"}},draggable:function(){return this.tags.length>1},skip:function(){return this.tags.map(function(t){return t.value})}},watch:{value:function(t){this.tags=this.prepareTags(t),this.onInvalid()}},mounted:function(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{addString:function(t){var e=this;if(t)if(t=t.trim(),t.includes(this.separator))t.split(this.separator).forEach(function(t){e.addString(t)});else if(0!==t.length)if("options"===this.accept){var n=this.options.filter(function(e){return e.text===t})[0];if(!n)return;this.addTag(n)}else this.addTag({text:t,value:t})},addTag:function(t){this.addTagToIndex(t),this.$refs.autocomplete.close(),this.$refs.input.focus()},addTagToIndex:function(t){if("options"===this.accept){var e=this.options.filter(function(e){return e.value===t.value})[0];if(!e)return}-1===this.index(t)&&(!this.max||this.tags.length0&&(t.preventDefault(),this.addString(this.newTag))},type:function(t){this.newTag=t,this.$refs.autocomplete.search(t)}},validations:function(){return{tags:{required:!this.required||es["required"],minLength:!this.min||Object(es["minLength"])(this.min),maxLength:!this.max||Object(es["maxLength"])(this.max)}}}},xa=ya,wa=(n("27c1"),Object(_["a"])(xa,$a,_a,!1,null,null,null)),Oa=wa.exports,Ca={extends:Is,props:Object(I["a"])({},Is.props,{autocomplete:{type:String,default:"tel"},type:{type:String,default:"tel"}})},Sa=Ca,Ea=Object(_["a"])(Sa,d,p,!1,null,null,null),ja=Ea.exports,Ta=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-textarea-input",attrs:{"data-theme":t.theme,"data-over":t.over}},[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({ref:"input",staticClass:"k-textarea-input-native",attrs:{"data-font":t.font,"data-size":t.size},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(e):null},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:e.ctrlKey?t.onSubmit(e):null},function(e){return e.metaKey?t.onShortcut(e):null},function(e){return e.ctrlKey?t.onShortcut(e):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)},Ia=[],La={inheritAttrs:!1,props:{autofocus:Boolean,buttons:{type:[Boolean,Array],default:!0},disabled:Boolean,endpoints:Object,font:String,id:[Number,String],name:[Number,String],maxlength:Number,minlength:Number,placeholder:String,preselect:Boolean,required:Boolean,size:String,spellcheck:{type:[Boolean,String],default:"off"},theme:String,uploads:[Boolean,Object,Array],value:String},data:function(){return{over:!1}},watch:{value:function(){var t=this;this.onInvalid(),this.$nextTick(function(){t.resize()})}},mounted:function(){var t=this;this.$nextTick(function(){t.$library.autosize(t.$refs.input)}),this.onInvalid(),this.$props.autofocus&&this.focus(),this.$props.preselect&&this.select()},methods:{cancel:function(){this.$refs.input.focus()},dialog:function(t){if(!this.$refs[t+"Dialog"])throw"Invalid toolbar dialog";this.$refs[t+"Dialog"].open(this.$refs.input,this.selection())},focus:function(){this.$refs.input.focus()},insert:function(t){var e=this,n=this.$refs.input,i=n.value;setTimeout(function(){if(n.focus(),document.execCommand("insertText",!1,t),n.value===i){var s=n.value.slice(0,n.selectionStart)+t+n.value.slice(n.selectionEnd);n.value=s,e.$emit("input",s)}}),this.resize()},insertFile:function(t){t&&t.length>0&&this.insert(t.map(function(t){return t.dragText}).join("\n\n"))},insertUpload:function(t,e){this.insert(e.map(function(t){return t.dragText}).join("\n\n")),this.$events.$emit("model.update")},onClick:function(){this.$refs.toolbar&&this.$refs.toolbar.close()},onCommand:function(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:function(t){if(this.$helper.isUploadEvent(t))return this.$refs.fileUpload.drop(t.dataTransfer.files,{url:A.api+"/"+this.endpoints.field+"/upload",multiple:!1});var e=this.$store.state.drag;e&&"text"===e.type&&(this.focus(),this.insert(e.data))},onFocus:function(t){this.$emit("focus",t)},onInput:function(t){this.$emit("input",t.target.value)},onInvalid:function(){this.$emit("invalid",this.$v.$invalid,this.$v)},onOut:function(){this.$refs.input.blur(),this.over=!1},onOver:function(t){if(this.uploads&&this.$helper.isUploadEvent(t))return t.dataTransfer.dropEffect="copy",this.focus(),void(this.over=!0);var e=this.$store.state.drag;e&&"text"===e.type&&(t.dataTransfer.dropEffect="copy",this.focus(),this.over=!0)},onShortcut:function(t){!1!==this.buttons&&"Meta"!==t.key&&"Control"!==t.key&&this.$refs.toolbar&&this.$refs.toolbar.shortcut(t.key,t)},onSubmit:function(t){return this.$emit("submit",t)},prepend:function(t){this.insert(t+" "+this.selection())},resize:function(){this.$library.autosize.update(this.$refs.input)},select:function(){this.$refs.select()},selectFile:function(){this.$refs.fileDialog.open({endpoint:this.endpoints.field+"/files",multiple:!1})},selection:function(){var t=this.$refs.input,e=t.selectionStart,n=t.selectionEnd;return t.value.substring(e,n)},uploadFile:function(){this.$refs.fileUpload.open({url:A.api+"/"+this.endpoints.field+"/upload",multiple:!1})},wrap:function(t){this.insert(t+this.selection()+t)}},validations:function(){return{value:{required:!this.required||es["required"],minLength:!this.minlength||Object(es["minLength"])(this.minlength),maxLength:!this.maxlength||Object(es["maxLength"])(this.maxlength)}}}},qa=La,Aa=(n("cca8"),Object(_["a"])(qa,Ta,Ia,!1,null,null,null)),Na=Aa.exports,Ba=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-time-input"},[n("k-select-input",{ref:"hour",attrs:{id:t.id,"aria-label":t.$t("hour"),autofocus:t.autofocus,options:t.hours,required:t.required,disabled:t.disabled,placeholder:"––"},on:{input:t.setHour,invalid:t.onInvalid},model:{value:t.hour,callback:function(e){t.hour=e},expression:"hour"}}),n("span",{staticClass:"k-time-input-separator"},[t._v(":")]),n("k-select-input",{ref:"minute",attrs:{"aria-label":t.$t("minutes"),options:t.minutes,required:t.required,disabled:t.disabled,placeholder:"––"},on:{input:t.setMinute,invalid:t.onInvalid},model:{value:t.minute,callback:function(e){t.minute=e},expression:"minute"}}),12===t.notation?n("k-select-input",{ref:"meridiem",staticClass:"k-time-input-meridiem",attrs:{"aria-label":t.$t("meridiem"),empty:!1,options:[{value:"AM",text:"AM"},{value:"PM",text:"PM"}],required:t.required,disabled:t.disabled},on:{input:t.onInput},model:{value:t.meridiem,callback:function(e){t.meridiem=e},expression:"meridiem"}}):t._e()],1)},Pa=[],Da={inheritAttrs:!1,props:{autofocus:Boolean,disabled:Boolean,id:[String,Number],notation:{type:Number,default:24},required:Boolean,step:{type:Number,default:5},value:{type:String}},data:function(){var t=this.toObject(this.value);return{time:this.value,hour:t.hour,minute:t.minute,meridiem:t.meridiem}},computed:{hours:function(){return this.options(24===this.notation?0:1,24===this.notation?23:12)},minutes:function(){return this.options(0,59,this.step)}},watch:{value:function(t){this.time=t},time:function(t){var e=this.toObject(t);this.hour=e.hour,this.minute=e.minute,this.meridiem=e.meridiem}},methods:{focus:function(){this.$refs.hour.focus()},setHour:function(t){t&&!this.minute&&(this.minute=0),t||(this.minute=null),this.onInput()},setMinute:function(t){t&&!this.hour&&(this.hour=0),t||(this.hour=null),this.onInput()},onInput:function(){if(null!==this.hour&&null!==this.minute){var t=this.$helper.pad(this.hour||0),e=this.$helper.pad(this.minute||0),n=String(this.meridiem||"AM").toUpperCase(),i=24===this.notation?"".concat(t,":").concat(e,":00"):"".concat(t,":").concat(e,":00 ").concat(n),s=24===this.notation?"HH:mm:ss":"hh:mm:ss A",a=this.$library.dayjs("2000-01-01 "+i,"YYYY-MM-DD "+s);this.$emit("input",a.format("HH:mm"))}else this.$emit("input","")},onInvalid:function(t,e){this.$emit("invalid",t,e)},options:function(t,e){for(var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1,i=[],s=t;s<=e;s+=n)i.push({value:s,text:this.$helper.pad(s)});return i},reset:function(){this.hour=null,this.minute=null,this.meridiem=null},round:function(t){return Math.floor(t/this.step)*this.step},toObject:function(t){var e=this.$library.dayjs("2001-01-01 "+t+":00","YYYY-MM-DD HH:mm:ss");return t&&!1!==e.isValid()?{hour:e.format(24===this.notation?"H":"h"),minute:this.round(e.format("m")),meridiem:e.format("A")}:{hour:null,minute:null,meridiem:null}}}},Ra=Da,Ma=(n("50da"),Object(_["a"])(Ra,Ba,Pa,!1,null,null,null)),za=Ma.exports,Ua=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:{disabled:t.disabled,id:t.id,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)}})])},Fa=[],Ha={inheritAttrs:!1,props:{autofocus:Boolean,disabled:Boolean,id:[Number,String],text:{type:[Array,String],default:function(){return[this.$t("off"),this.$t("on")]}},required:Boolean,value:Boolean},computed:{label:function(){return xt()(this.text)?this.value?this.text[1]:this.text[0]:this.text}},watch:{value:function(){this.onInvalid()}},mounted:function(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus:function(){this.$refs.input.focus()},onEnter:function(t){"Enter"===t.key&&this.$refs.input.click()},onInput:function(t){this.$emit("input",t)},onInvalid:function(){this.$emit("invalid",this.$v.$invalid,this.$v)},select:function(){this.$refs.input.focus()}},validations:function(){return{value:{required:!this.required||es["required"]}}}},Ka=Ha,Va=(n("bb41"),Object(_["a"])(Ka,Ua,Fa,!1,null,null,null)),Ya=Va.exports,Wa={extends:Is,props:Object(I["a"])({},Is.props,{autocomplete:{type:String,default:"url"},type:{type:String,default:"url"}})},Ga=Wa,Ja=Object(_["a"])(Ga,f,h,!1,null,null,null),Za=Ja.exports,Xa=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"}},"k-input",t.$props,!1),t.$listeners))],1)},Qa=[],to={inheritAttrs:!1,props:Object(I["a"])({},Li.props,Hi.props,ds.props,{counter:{type:Boolean,default:!0}}),computed:{counterOptions:function(){return null!==this.value&&!this.disabled&&!1!==this.counter&&{count:this.value&&xt()(this.value)?this.value.length:0,min:this.min,max:this.max}}},methods:{focus:function(){this.$refs.input.focus()}}},eo=to,no=Object(_["a"])(eo,Xa,Qa,!1,null,null,null),io=no.exports,so=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("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,type:t.inputType,value:t.date,theme:"field"}},"k-input",t.$props,!1),t.listeners),[n("template",{slot:"icon"},[n("k-dropdown",[n("k-button",{staticClass:"k-input-icon-button",attrs:{icon:t.icon,tooltip:t.$t("date.select"),tabindex:"-1"},on:{click:function(e){return t.$refs.dropdown.toggle()}}}),n("k-dropdown-content",{ref:"dropdown",attrs:{align:"right"}},[n("k-calendar",{attrs:{value:t.date},on:{input:function(e){t.onInput(e),t.$refs.dropdown.close()}}})],1)],1)],1)],2)],1)},ao=[],oo={inheritAttrs:!1,props:Object(I["a"])({},Li.props,Hi.props,Os.props,{icon:{type:String,default:"calendar"}}),data:function(){return{date:this.value,listeners:Object(I["a"])({},this.$listeners,{input:this.onInput})}},computed:{inputType:function(){return!1===this.time?"date":"datetime"}},watch:{value:function(t){this.date=t}},methods:{focus:function(){this.$refs.input.focus()},onInput:function(t){this.date=t,this.$emit("input",t)}}},ro=oo,lo=Object(_["a"])(ro,so,ao,!1,null,null,null),uo=lo.exports,co=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"}},"k-input",t.$props,!1),t.$listeners),[t.link?n("k-button",{staticClass:"k-input-icon-button",attrs:{slot:"icon",icon:t.icon,link:t.mailto,tooltip:t.$t("open"),tabindex:"-1",target:"_blank"},slot:"icon"}):t._e()],1)],1)},po=[],fo={inheritAttrs:!1,props:Object(I["a"])({},Li.props,Hi.props,Ns.props,{link:{type:Boolean,default:!0},icon:{type:String,default:"email"}}),computed:{mailto:function(){return this.value&&this.value.length>0?"mailto:"+this.value:null}},methods:{focus:function(){this.$refs.input.focus()}}},ho=fo,mo=Object(_["a"])(ho,co,po,!1,null,null,null),go=mo.exports,bo=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-files-field"},"k-field",t.$props,!1),[t.more&&!t.disabled?n("template",{slot:"options"},[n("k-button-group",{staticClass:"k-field-options"},[t.uploads?[n("k-dropdown",[n("k-button",{ref:"pickerToggle",staticClass:"k-field-options-button",attrs:{icon:"add"},on:{click:function(e){return t.$refs.picker.toggle()}}},[t._v("\n "+t._s(t.$t("add"))+"\n ")]),n("k-dropdown-content",{ref:"picker",attrs:{align:"right"}},[n("k-dropdown-item",{attrs:{icon:"check"},on:{click:t.open}},[t._v(t._s(t.$t("select")))]),n("k-dropdown-item",{attrs:{icon:"upload"},on:{click:t.upload}},[t._v(t._s(t.$t("upload")))])],1)],1)]:[n("k-button",{staticClass:"k-field-options-button",attrs:{icon:"add"},on:{click:t.open}},[t._v(t._s(t.$t("add")))])]],2)],1):t._e(),t.selected.length?[n("k-draggable",{attrs:{element:t.elements.list,list:t.selected,"data-size":t.size,handle:!0,"data-invalid":t.isInvalid},on:{end:t.onInput}},t._l(t.selected,function(e,i){return n(t.elements.item,{key:e.filename,tag:"component",attrs:{sortable:!t.disabled&&t.selected.length>1,text:e.text,link:e.link,info:e.info,image:e.image,icon:e.icon}},[t.disabled?t._e():n("k-button",{attrs:{slot:"options",tooltip:t.$t("remove"),icon:"remove"},on:{click:function(e){return t.remove(i)}},slot:"options"})],1)}),1)]:n("k-empty",{attrs:{layout:t.layout,"data-invalid":t.isInvalid,icon:"image"},on:{click:t.open}},[t._v("\n "+t._s(t.empty||t.$t("field.files.empty"))+"\n ")]),n("k-files-dialog",{ref:"selector",on:{submit:t.select}}),n("k-upload",{ref:"fileUpload",on:{success:t.selectUpload}})],2)},vo=[],ko={inheritAttrs:!1,props:Object(I["a"])({},Li.props,{empty:String,info:String,layout:String,max:Number,multiple:Boolean,parent:String,search:Boolean,size:String,text:String,value:{type:Array,default:function(){return[]}}}),data:function(){return{selected:this.value}},computed:{elements:function(){var t={cards:{list:"k-cards",item:"k-card"},list:{list:"k-list",item:"k-list-item"}};return t[this.layout]?t[this.layout]:t["list"]},isInvalid:function(){return!(!this.required||0!==this.selected.length)||(!!(this.min&&this.selected.lengththis.max))},more:function(){return!this.max||this.max>this.selected.length}},watch:{value:function(t){this.selected=t}},methods:{focus:function(){},onInput:function(){this.$emit("input",this.selected)},remove:function(t){this.selected.splice(t,1),this.onInput()},removeById:function(t){this.selected=this.selected.filter(function(e){return e.id!==t}),this.onInput()},select:function(t){var e=this;0!==t.length?(this.selected=this.selected.filter(function(e){return t.filter(function(t){return t.id===e.id}).length>0}),t.forEach(function(t){0===e.selected.filter(function(e){return t.id===e.id}).length&&e.selected.push(t)}),this.onInput()):this.selected=[]}}},$o={mixins:[ko],props:{uploads:[Boolean,Object,Array]},created:function(){this.$events.$on("file.delete",this.removeById)},destroyed:function(){this.$events.$off("file.delete",this.removeById)},methods:{prompt:function(t){t.stopPropagation(),this.uploads?this.$refs.picker.toggle():this.open()},open:function(){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(function(t){return t.id})})},selectUpload:function(t,e){var n=this;!1===this.multiple&&(this.selected=[]),e.forEach(function(t){n.selected.push(t)}),this.onInput(),this.$events.$emit("model.update")},upload:function(){this.$refs.fileUpload.open({url:A.api+"/"+this.endpoints.field+"/upload",multiple:this.multiple,accept:this.uploads.accept})}}},_o=$o,yo=(n("4a4b"),Object(_["a"])(_o,bo,vo,!1,null,null,null)),xo=yo.exports,wo=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-headline",{staticClass:"k-headline-field",attrs:{"data-numbered":t.numbered,size:"large"}},[t._v("\n "+t._s(t.label)+"\n")])},Oo=[],Co={props:{label:String,numbered:Boolean}},So=Co,Eo=(n("19d7"),Object(_["a"])(So,wo,Oo,!1,null,null,null)),jo=Eo.exports,To=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)],1)},Io=[],Lo={props:{label:String,text:String,theme:{type:String,default:"info"}}},qo=Lo,Ao=(n("ddfd"),Object(_["a"])(qo,To,Io,!1,null,null,null)),No=Ao.exports,Bo=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("hr",{staticClass:"k-line-field"})},Po=[],Do=(n("718c"),{}),Ro=Object(_["a"])(Do,Bo,Po,!1,null,null,null),Mo=Ro.exports,zo=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(e))}}},"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)},Uo=[],Fo={inheritAttrs:!1,props:Object(I["a"])({},Li.props,Hi.props,zs.props,{counter:{type:Boolean,default:!0},icon:{type:String,default:"angle-down"}}),computed:{counterOptions:function(){return null!==this.value&&!this.disabled&&!1!==this.counter&&{count:this.value&&xt()(this.value)?this.value.length:0,min:this.min,max:this.max}}},mounted:function(){this.$refs.input.$el.setAttribute("tabindex",0)},methods:{blur:function(t){this.$refs.input.blur(t)},focus:function(){this.$refs.input.focus()}}},Ho=Fo,Ko=Object(_["a"])(Ho,zo,Uo,!1,null,null,null),Vo=Ko.exports,Yo=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"}},"k-input",t.$props,!1),t.$listeners))],1)},Wo=[],Go={inheritAttrs:!1,props:Object(I["a"])({},Li.props,Hi.props,Zs.props),methods:{focus:function(){this.$refs.input.focus()}}},Jo=Go,Zo=Object(_["a"])(Jo,Yo,Wo,!1,null,null,null),Xo=Zo.exports,Qo=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-pages-field"},"k-field",t.$props,!1),[n("k-button-group",{staticClass:"k-field-options",attrs:{slot:"options"},slot:"options"},[t.more&&!t.disabled?n("k-button",{staticClass:"k-field-options-button",attrs:{icon:"add"},on:{click:t.open}},[t._v("\n "+t._s(t.$t("select"))+"\n ")]):t._e()],1),t.selected.length?[n("k-draggable",{attrs:{element:t.elements.list,handle:!0,list:t.selected,"data-size":t.size,"data-invalid":t.isInvalid},on:{end:t.onInput}},t._l(t.selected,function(e,i){return n(t.elements.item,{key:e.id,tag:"component",attrs:{sortable:!t.disabled&&t.selected.length>1,text:e.text,info:e.info,link:e.link,icon:e.icon,image:e.image}},[t.disabled?t._e():n("k-button",{attrs:{slot:"options",icon:"remove"},on:{click:function(e){return t.remove(i)}},slot:"options"})],1)}),1)]:n("k-empty",{attrs:{layout:t.layout,"data-invalid":t.isInvalid,icon:"page"},on:{click:t.open}},[t._v("\n "+t._s(t.empty||t.$t("field.pages.empty"))+"\n ")]),n("k-pages-dialog",{ref:"selector",on:{submit:t.select}})],2)},tr=[],er={mixins:[ko],methods:{open:function(){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(function(t){return t.id})})}}},nr=er,ir=(n("7e85"),Object(_["a"])(nr,Qo,tr,!1,null,null,null)),sr=ir.exports,ar=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}},"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)},or=[],rr={inheritAttrs:!1,props:Object(I["a"])({},Li.props,Hi.props,ea.props,{counter:{type:Boolean,default:!0},minlength:{type:Number,default:8},icon:{type:String,default:"key"}}),computed:{counterOptions:function(){return null!==this.value&&!this.disabled&&!1!==this.counter&&{count:this.value?String(this.value).length:0,min:this.minlength,max:this.maxlength}}},methods:{focus:function(){this.$refs.input.focus()}}},lr=rr,ur=Object(_["a"])(lr,ar,or,!1,null,null,null),cr=ur.exports,dr=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"}},"k-input",t.$props,!1),t.$listeners))],1)},pr=[],fr={inheritAttrs:!1,props:Object(I["a"])({},Li.props,Hi.props,ra.props),methods:{focus:function(){this.$refs.input.focus()}}},hr=fr,mr=Object(_["a"])(hr,dr,pr,!1,null,null,null),gr=mr.exports,br=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"}},"k-input",t.$props,!1),t.$listeners))],1)},vr=[],kr={inheritAttrs:!1,props:Object(I["a"])({},Li.props,Hi.props,fa.props),methods:{focus:function(){this.$refs.input.focus()}}},$r=kr,_r=Object(_["a"])($r,br,vr,!1,null,null,null),yr=_r.exports,xr=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"}},"k-input",t.$props,!1),t.$listeners))],1)},wr=[],Or={inheritAttrs:!1,props:Object(I["a"])({},Li.props,Hi.props,ka.props,{icon:{type:String,default:"angle-down"}}),methods:{focus:function(){this.$refs.input.focus()}}},Cr=Or,Sr=Object(_["a"])(Cr,xr,wr,!1,null,null,null),Er=Sr.exports,jr=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()}}},"k-field",t.$props,!1),[n("template",{slot:"options"},[t.more&&null===t.currentIndex?n("k-button",{ref:"add",attrs:{id:t._uid,icon:"add"},on:{click:t.add}},[t._v("\n "+t._s(t.$t("add"))+"\n ")]):t._e()],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:{icon:"cancel"},on:{click:t.close}},[t._v(t._s(t.$t("cancel")))]),"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:{icon:"check"},on:{click:t.submit}},[t._v(t._s(t.$t("new"!==t.currentIndex?"confirm":"add")))])],1)],1)]:0===t.items.length?n("k-empty",{attrs:{"data-invalid":t.isInvalid,icon:"list-bullet"},on:{click:t.add}},[t._v("\n "+t._s(t.empty||t.$t("field.structure.empty"))+"\n ")]):[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,i){return n("th",{key:i+"-header",staticClass:"k-structure-table-column",style:"width:"+t.width(e.width),attrs:{"data-align":e.align}},[t._v("\n "+t._s(e.label)+"\n ")])}),n("th")],2)]),n("k-draggable",{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,i){return n("tr",{key:i,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(i)))])],1),t._l(t.columns,function(s,a){return n("td",{key:a,staticClass:"k-structure-table-column",style:"width:"+t.width(s.width),attrs:{title:s.label,"data-align":s.align},on:{click:function(e){return t.jump(i,a)}}},[!1===t.columnIsEmpty(e[a])?[t.previewExists(s.type)?n("k-"+s.type+"-field-preview",{tag:"component",attrs:{value:e[a],column:s,field:t.fields[a]},on:{input:function(e){return t.update(i,a,e)}}}):[n("p",{staticClass:"k-structure-table-text"},[t._v("\n "+t._s(s.before)+" "+t._s(t.displayText(t.fields[a],e[a])||"–")+" "+t._s(s.after)+"\n ")])]]:t._e()],2)}),n("td",{staticClass:"k-structure-table-option"},[n("k-button",{attrs:{tooltip:t.$t("remove"),icon:"remove"},on:{click:function(e){return t.confirmRemove(i)}}})],1)],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:{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)},Tr=[];Array.prototype.sortBy=function(t){var e=z["a"].prototype.$helper.sort(),n=t.split(" "),i=n[0],s=n[1]||"asc";return this.sort(function(t,n){var a=String(t[i]).toLowerCase(),o=String(n[i]).toLowerCase();return"desc"===s?e(o,a):e(a,o)})};var Ir,Lr,qr,Ar,Nr={inheritAttrs:!1,props:Object(I["a"])({},Li.props,{columns:Object,empty:String,fields:Object,limit:Number,max:Number,min:Number,sortable:{type:Boolean,default:!0},sortBy:String,value:{type:Array,default:function(){return[]}}}),data:function(){return{items:this.makeItems(this.value),currentIndex:null,currentModel:null,trash:null,page:1}},computed:{dragOptions:function(){return{disabled:!this.isSortable,fallbackClass:"k-sortable-row-fallback"}},formFields:function(){var t=this,e={};return kt()(this.fields).forEach(function(n){var i=t.fields[n];i.section=t.name,i.endpoints={field:t.endpoints.field+"+"+n,section:t.endpoints.section,model:t.endpoints.model},e[n]=i}),e},more:function(){return!0!==this.disabled&&!(this.max&&this.items.length>=this.max)},isInvalid:function(){return!0!==this.disabled&&(!!(this.min&&this.items.lengththis.max))},isSortable:function(){return!this.sortBy&&(!this.limit&&(!0!==this.disabled&&(!(this.items.length<=1)&&!1!==this.sortable)))},pagination:function(){var 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:function(){return this.limit?this.items.slice(this.pagination.offset,this.pagination.offset+this.limit):this.items}},watch:{value:function(t){t!=this.items&&(this.items=this.makeItems(t))}},methods:{add:function(){var t=this;if(!0===this.disabled)return!1;if(null!==this.currentIndex)return this.escape(),!1;var e={};kt()(this.fields).forEach(function(n){var i=t.fields[n];null!==i.default?e[n]=t.$helper.clone(i.default):e[n]=null}),this.currentIndex="new",this.currentModel=e,this.createForm()},close:function(){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")},columnIsEmpty:function(t){return void 0===t||null===t||""===t||("object"===Object(Ht["a"])(t)&&0===kt()(t).length&&t.constructor===Object||void 0!==t.length&&0===t.length)},confirmRemove:function(t){this.close(),this.trash=t,this.$refs.remove.open()},createForm:function(t){var e=this;this.$events.$on("keydown.esc",this.escape),this.$events.$on("keydown.cmd.s",this.submit),this.$store.dispatch("content/disable"),this.$nextTick(function(){e.$refs.form&&e.$refs.form.focus(t)})},displayText:function(t,e){switch(t.type){case"user":return e.email;case"date":var n=this.$library.dayjs(e),i=!0===t.time?"YYYY-MM-DD HH:mm":"YYYY-MM-DD";return n.isValid()?n.format(i):"";case"tags":case"multiselect":return e.map(function(t){return t.text}).join(", ");case"checkboxes":return e.map(function(e){var n=e;return t.options.forEach(function(t){t.value===e&&(n=t.text)}),n}).join(", ");case"radio":case"select":var s=t.options.filter(function(t){return t.value===e})[0];return s?s.text:null}return"object"===Object(Ht["a"])(e)&&null!==e?"…":e},escape:function(){var t=this;if("new"===this.currentIndex){var e=_t()(this.currentModel),n=!0;if(e.forEach(function(e){!1===t.columnIsEmpty(e)&&(n=!1)}),!0===n)return void this.close()}this.submit()},focus:function(){this.$refs.add&&this.$refs.add.focus&&this.$refs.add.focus()},indexOf:function(t){return this.limit?(this.page-1)*this.limit+t+1:t+1},isActive:function(t){return this.currentIndex===t},jump:function(t,e){this.open(t+this.pagination.offset,e)},makeItems:function(t){return!1===xt()(t)?[]:this.sort(t)},onInput:function(){this.$emit("input",this.items)},open:function(t,e){this.currentIndex=t,this.currentModel=this.$helper.clone(this.items[t]),this.createForm(e)},beforePaginate:function(){return this.save(this.currentModel)},paginate:function(t){this.open(t.offset)},paginateItems:function(t){this.page=t.page},previewExists:function(t){return void 0!==z["a"].options.components["k-"+t+"-field-preview"]||void 0!==this.$options.components["k-"+t+"-field-preview"]},remove:function(){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:function(t){return this.sortBy?t.sortBy(this.sortBy):t},save:function(){var t=this;return null!==this.currentIndex&&void 0!==this.currentIndex?this.validate(this.currentModel).then(function(){return"new"===t.currentIndex?t.items.push(t.currentModel):t.items[t.currentIndex]=t.currentModel,t.items=t.sort(t.items),t.onInput(),!0}).catch(function(e){throw t.$store.dispatch("notification/error",{message:t.$t("error.form.incomplete"),details:e}),e}):Je.a.resolve()},submit:function(){this.save().then(this.close).catch(function(){})},validate:function(t){return this.$api.post(this.endpoints.field+"/validate",t).then(function(t){if(t.length>0)throw t;return!0})},width:function(t){if(!t)return"auto";var e=t.toString().split("/");if(2!==e.length)return"auto";var n=Number(e[0]),i=Number(e[1]);return Ys()(100/i*n,2).toFixed(2)+"%"},update:function(t,e,n){this.items[t][e]=n,this.onInput()}}},Br=Nr,Pr=(n("088c"),Object(_["a"])(Br,jr,Tr,!1,null,null,null)),Dr=Pr.exports,Rr=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"}},"k-input",t.$props,!1),t.$listeners))],1)},Mr=[],zr={inheritAttrs:!1,props:Object(I["a"])({},Li.props,Hi.props,Oa.props,{counter:{type:Boolean,default:!0}}),computed:{counterOptions:function(){return null!==this.value&&!this.disabled&&!1!==this.counter&&{count:this.value&&xt()(this.value)?this.value.length:0,min:this.min,max:this.max}}},methods:{focus:function(){this.$refs.input.focus()}}},Ur=zr,Fr=Object(_["a"])(Ur,Rr,Mr,!1,null,null,null),Hr=Fr.exports,Kr=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"}},"k-input",t.$props,!1),t.$listeners))],1)},Vr=[],Yr={inheritAttrs:!1,props:Object(I["a"])({},Li.props,Hi.props,ja.props,{icon:{type:String,default:"phone"}}),methods:{focus:function(){this.$refs.input.focus()}}},Wr=Yr,Gr=Object(_["a"])(Wr,Kr,Vr,!1,null,null,null),Jr=Gr.exports,Zr=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}},"k-field",t.$props,!1),[t._t("options",null,{slot:"options"}),n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field"}},"k-input",t.$props,!1),t.$listeners))],2)},Xr=[],Qr={inheritAttrs:!1,props:Object(I["a"])({},Li.props,Hi.props,Is.props,{counter:{type:Boolean,default:!0}}),computed:{counterOptions:function(){return null!==this.value&&!this.disabled&&!1!==this.counter&&{count:this.value?String(this.value).length:0,min:this.minlength,max:this.maxlength}}},methods:{focus:function(){this.$refs.input.focus()}}},tl=Qr,el=(n("b746"),Object(_["a"])(tl,Zr,Xr,!1,null,null,null)),nl=el.exports,il=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)},sl=[],al={inheritAttrs:!1,props:Object(I["a"])({},Li.props,Hi.props,Na.props,{counter:{type:Boolean,default:!0}}),computed:{counterOptions:function(){return null!==this.value&&!this.disabled&&!1!==this.counter&&{count:this.value?this.value.length:0,min:this.minlength,max:this.maxlength}}},methods:{focus:function(){this.$refs.input.focus()}}},ol=al,rl=Object(_["a"])(ol,il,sl,!1,null,null,null),ll=rl.exports,ul=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._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field"}},"k-input",t.$props,!1),t.$listeners))],1)},cl=[],dl={inheritAttrs:!1,props:Object(I["a"])({},Li.props,Hi.props,za.props,{icon:{type:String,default:"clock"}}),methods:{focus:function(){this.$refs.input.focus()}}},pl=dl,fl=Object(_["a"])(pl,ul,cl,!1,null,null,null),hl=fl.exports,ml=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"}},"k-input",t.$props,!1),t.$listeners))],1)},gl=[],bl={inheritAttrs:!1,props:Object(I["a"])({},Li.props,Hi.props,Ya.props),methods:{focus:function(){this.$refs.input.focus()}}},vl=bl,kl=Object(_["a"])(vl,ml,gl,!1,null,null,null),$l=kl.exports,_l=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"}},"k-input",t.$props,!1),t.$listeners),[t.link?n("k-button",{staticClass:"k-input-icon-button",attrs:{slot:"icon",icon:t.icon,link:t.value,tooltip:t.$t("open"),tabindex:"-1",target:"_blank"},slot:"icon"}):t._e()],1)],1)},yl=[],xl={inheritAttrs:!1,props:Object(I["a"])({},Li.props,Hi.props,Za.props,{link:{type:Boolean,default:!0},icon:{type:String,default:"url"}}),methods:{focus:function(){this.$refs.input.focus()}}},wl=xl,Ol=Object(_["a"])(wl,_l,yl,!1,null,null,null),Cl=Ol.exports,Sl=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-users-field"},"k-field",t.$props,!1),[n("k-button-group",{staticClass:"k-field-options",attrs:{slot:"options"},slot:"options"},[t.more&&!t.disabled?n("k-button",{staticClass:"k-field-options-button",attrs:{icon:"add"},on:{click:t.open}},[t._v("\n "+t._s(t.$t("select"))+"\n ")]):t._e()],1),t.selected.length?[n("k-draggable",{attrs:{element:t.elements.list,list:t.selected,handle:!0,"data-invalid":t.isInvalid},on:{end:t.onInput}},t._l(t.selected,function(e,i){return n(t.elements.item,{key:e.email,tag:"component",attrs:{sortable:!t.disabled&&t.selected.length>1,text:e.text,info:e.info,link:t.$api.users.link(e.id),image:e.image,icon:e.icon}},[t.disabled?t._e():n("k-button",{attrs:{slot:"options",icon:"remove"},on:{click:function(e){return t.remove(i)}},slot:"options"})],1)}),1)]:n("k-empty",{attrs:{"data-invalid":t.isInvalid,icon:"users"},on:{click:t.open}},[t._v("\n "+t._s(t.empty||t.$t("field.users.empty"))+"\n ")]),n("k-users-dialog",{ref:"selector",on:{submit:t.select}})],2)},El=[],jl={mixins:[ko],methods:{open:function(){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(function(t){return t.id})})}}},Tl=jl,Il=(n("7f6e"),Object(_["a"])(Tl,Sl,El,!1,null,null,null)),Ll=Il.exports,ql=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,i){return[e.divider?[n("span",{key:i,staticClass:"k-toolbar-divider"})]:e.dropdown?[n("k-dropdown",{key:i},[n("k-button",{key:i,staticClass:"k-toolbar-button",attrs:{icon:e.icon,tooltip:e.label,tabindex:"-1"},on:{click:function(e){t.$refs[i+"-dropdown"][0].toggle()}}}),n("k-dropdown-content",{ref:i+"-dropdown",refInFor:!0},t._l(e.dropdown,function(e,i){return n("k-dropdown-item",{key:i,attrs:{icon:e.icon},on:{click:function(n){return t.command(e.command,e.args)}}},[t._v("\n "+t._s(e.label)+"\n ")])}),1)],1)]:[n("k-button",{key:i,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)])])},Al=[],Nl=function(t){this.command("insert",function(e,n){var i=[];return n.split("\n").forEach(function(e,n){var s="ol"===t?n+1+".":"-";i.push(s+" "+e)}),i.join("\n")})},Bl={layout:["headlines","bold","italic","|","link","email","file","|","code","ul","ol"],props:{buttons:{type:[Boolean,Array],default:!0},uploads:[Boolean,Object,Array]},data:function(){var t={},e={},n=[],i=this.commands();return!1===this.buttons?t:(xt()(this.buttons)&&(n=this.buttons),!0!==xt()(this.buttons)&&(n=this.$options.layout),n.forEach(function(n,s){if("|"===n)t["divider-"+s]={divider:!0};else if(i[n]){var a=i[n];t[n]=a,a.shortcut&&(e[a.shortcut]=n)}}),{layout:t,shortcuts:e})},methods:{command:function(t,e){"function"===typeof t?t.apply(this):this.$emit("command",t,e)},close:function(){var t=this;kt()(this.$refs).forEach(function(e){var n=t.$refs[e][0];n.close&&"function"===typeof n.close&&n.close()})},fileCommandSetup:function(){var 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:function(){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:"l",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:function(){return Nl.apply(this,["ul"])}},ol:{label:this.$t("toolbar.button.ol"),icon:"list-numbers",command:function(){return Nl.apply(this,["ol"])}}}},shortcut:function(t,e){if(this.shortcuts[t]){var n=this.layout[this.shortcuts[t]];if(!n)return!1;e.preventDefault(),this.command(n.command,n.args)}}}},Pl=Bl,Dl=(n("df0d"),Object(_["a"])(Pl,ql,Al,!1,null,null,null)),Rl=Dl.exports,Ml=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",attrs:{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)},zl=[],Ul={data:function(){return{value:{email:null,text:null},fields:{email:{label:this.$t("email"),type:"email"},text:{label:this.$t("link.text"),type:"text"}}}},computed:{kirbytext:function(){return this.$store.state.system.info.kirbytext}},methods:{open:function(t,e){this.value.text=e,this.$refs.dialog.open()},cancel:function(){this.$emit("cancel")},createKirbytext:function(){var t=this.value.email||"";return this.value.text&&this.value.text.length>0?"(email: ".concat(t," text: ").concat(this.value.text,")"):"(email: ".concat(t,")")},createMarkdown:function(){var t=this.value.email||"";return this.value.text&&this.value.text.length>0?"[".concat(this.value.text,"](mailto:").concat(t,")"):"<".concat(t,">")},submit:function(){this.$emit("submit",this.kirbytext?this.createKirbytext():this.createMarkdown()),this.value={email:null,text:null},this.$refs.dialog.close()}}},Fl=Ul,Hl=Object(_["a"])(Fl,Ml,zl,!1,null,null,null),Kl=Hl.exports,Vl=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",attrs:{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)},Yl=[],Wl={data:function(){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:function(){return this.$store.state.system.info.kirbytext}},methods:{open:function(t,e){this.value.text=e,this.$refs.dialog.open()},cancel:function(){this.$emit("cancel")},createKirbytext:function(){return this.value.text.length>0?"(link: ".concat(this.value.url," text: ").concat(this.value.text,")"):"(link: ".concat(this.value.url,")")},createMarkdown:function(){return this.value.text.length>0?"[".concat(this.value.text,"](").concat(this.value.url,")"):"<".concat(this.value.url,">")},submit:function(){this.$emit("submit",this.kirbytext?this.createKirbytext():this.createMarkdown()),this.value={url:null,text:null},this.$refs.dialog.close()}}},Gl=Wl,Jl=Object(_["a"])(Gl,Vl,Yl,!1,null,null,null),Zl=Jl.exports,Xl=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(e){return n("li",{key:e.url},[n("k-link",{attrs:{title:e.filename,to:e.link},nativeOn:{click:function(t){t.stopPropagation()}}},["image"===e.type?n("k-image",t._b({},"k-image",t.imageOptions(e),!1)):n("k-icon",t._b({},"k-icon",e.icon,!1))],1)],1)}),0):t._e()},Ql=[],tu=function(t){if(!t)return!1;var e=null,n=null;return t.list?(e=t.list.url,n=t.list.srcset):(e=t.url,n=t.srcset),!!e&&{src:e,srcset:n,back:t.back||"black",cover:t.cover}},eu={props:{value:Array,field:Object},methods:{imageOptions:function(t){var e=tu(t.image);return e.src?Object(I["a"])({},e,{back:"pattern",cover:!1},this.field.image||{}):{src:t.url}}}},nu=eu,iu=(n("21dc"),Object(_["a"])(nu,Xl,Ql,!1,null,null,null)),su=iu.exports,au=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("p",{staticClass:"k-url-field-preview"},[t._v("\n "+t._s(t.column.before)+"\n "),n("k-link",{attrs:{to:t.link,target:"_blank"},nativeOn:{click:function(t){t.stopPropagation()}}},[t._v(t._s(t.value))]),t._v("\n "+t._s(t.column.after)+"\n")],1)},ou=[],ru={props:{column:{type:Object,default:function(){return{}}},value:String},computed:{link:function(){return this.value}}},lu=ru,uu=(n("977f"),Object(_["a"])(lu,au,ou,!1,null,null,null)),cu=uu.exports,du={extends:cu,computed:{link:function(){return this.value&&this.value.length>0?"mailto:"+this.value:null}}},pu=du,fu=Object(_["a"])(pu,Ir,Lr,!1,null,null,null),hu=fu.exports,mu=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:t.$api.pages.link(e.id)},nativeOn:{click:function(t){t.stopPropagation()}}},[n("k-icon",{staticClass:"k-pages-field-preview-image",attrs:{type:"page",back:"pattern"}}),n("figcaption",[t._v("\n "+t._s(e.text)+"\n ")])],1)],1)])}),0):t._e()},gu=[],bu={props:{value:Array}},vu=bu,ku=(n("d0c1"),Object(_["a"])(vu,mu,gu,!1,null,null,null)),$u=ku.exports,_u=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("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"}})},yu=[],xu={props:{field:Object,value:Boolean,column:Object},computed:{text:function(){return!1!==this.column.text?this.field.text:null}}},wu=xu,Ou=(n("1c4e"),Object(_["a"])(wu,_u,yu,!1,null,null,null)),Cu=Ou.exports,Su=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:t.$api.users.link(e.id)},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("\n "+t._s(e.username)+"\n ")])],1)],1)])}),0):t._e()},Eu=[],ju={props:{value:Array}},Tu=ju,Iu=(n("3a85"),Object(_["a"])(Tu,Su,Eu,!1,null,null,null)),Lu=Iu.exports,qu=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()])},Au=[],Nu=(n("6f7b"),{}),Bu=Object(_["a"])(Nu,qu,Au,!1,null,null,null),Pu=Bu.exports,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",[n("k-text",{domProps:{innerHTML:t._s(t.text)}})])],2)},Ru=[],Mu={props:{theme:String,text:String}},zu=Mu,Uu=(n("7dc7"),Object(_["a"])(zu,Du,Ru,!1,null,null,null)),Fu=Uu.exports,Hu=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("figure",t._g({staticClass:"k-card"},t.$listeners),[t.sortable?n("k-sort-handle"):t._e(),n(t.wrapper,{tag:"component",attrs:{to:t.link,target:t.target}},[t.imageOptions?n("k-image",t._b({staticClass:"k-card-image"},"k-image",t.imageOptions,!1)):n("span",{staticClass:"k-card-icon",style:"padding-bottom:"+t.ratioPadding},[n("k-icon",t._b({},"k-icon",t.icon,!1))],1),n("figcaption",{staticClass:"k-card-content"},[n("span",{staticClass:"k-card-text",attrs:{"data-noinfo":!t.info}},[t._v(t._s(t.text))]),t.info?n("span",{staticClass:"k-card-info",domProps:{innerHTML:t._s(t.info)}}):t._e()])],1),n("nav",{staticClass:"k-card-options"},[t.flag?n("k-button",t._b({staticClass:"k-card-options-button",on:{click:t.flag.click}},"k-button",t.flag,!1)):t._e(),t._t("options",[t.options?n("k-button",{staticClass:"k-card-options-button",attrs:{tooltip:t.$t("options"),icon:"dots"},on:{click:function(e){return e.stopPropagation(),t.$refs.dropdown.toggle()}}}):t._e(),n("k-dropdown-content",{ref:"dropdown",staticClass:"k-card-options-dropdown",attrs:{options:t.options,align:"right"},on:{action:function(e){return t.$emit("action",e)}}})])],2)],1)},Ku=[],Vu={inheritAttrs:!1,props:{column:String,flag:Object,icon:{type:Object,default:function(){return{type:"file",back:"black"}}},image:Object,info:String,link:[String,Function],options:[Array,Function],sortable:Boolean,target:String,text:String},computed:{wrapper:function(){return this.link?"k-link":"div"},ratioPadding:function(){return this.icon&&this.icon.ratio?this.$helper.ratio(this.icon.ratio):this.$helper.ratio("3/2")},imageOptions:function(){if(!this.image)return!1;var t=null,e=null;return this.image.cards?(t=this.image.cards.url,e=this.image.cards.srcset):(t=this.image.url,e=this.image.srcset),!!t&&{src:t,srcset:e,back:this.image.back||"black",cover:this.image.cover,ratio:this.image.ratio||"3/2",sizes:this.getSizes(this.column)}}},methods:{getSizes:function(t){switch(t){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"}}}},Yu=Vu,Wu=(n("c119"),Object(_["a"])(Yu,Hu,Ku,!1,null,null,null)),Gu=Wu.exports,Ju=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-cards"},[t._t("default",t._l(t.cards,function(e,i){return n("k-card",t._g(t._b({key:i},"k-card",e,!1),t.$listeners))}))],2)},Zu=[],Xu={props:{cards:Array}},Qu=Xu,tc=(n("f56d"),Object(_["a"])(Qu,Ju,Zu,!1,null,null,null)),ec=tc.exports,nc=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-collection",attrs:{"data-layout":t.layout}},[n("k-draggable",{attrs:{list:t.items,options:t.dragOptions,element:t.elements.list,"data-size":t.size,handle:!0},on:{change:function(e){return t.$emit("change",e)},end:t.onEnd}},t._l(t.items,function(e,i){return n(t.elements.item,t._b({key:i,tag:"component",class:{"k-draggable-item":e.sortable},on:{action:function(n){return t.$emit("action",e,n)},dragstart:function(n){return t.onDragStart(n,e.dragText)}}},"component",e,!1))}),1),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)},ic=[],sc={props:{help:String,items:{type:[Array,Object],default:function(){return[]}},layout:{type:String,default:"list"},size:String,sortable:Boolean,pagination:{type:[Boolean,Object],default:function(){return!1}}},computed:{hasPagination:function(){return!1!==this.pagination&&(!0!==this.paginationOptions.hide&&!(this.pagination.total<=this.pagination.limit))},hasFooter:function(){return!(!this.hasPagination&&!this.help)},dragOptions:function(){return{sort:this.sortable,disabled:!1===this.sortable,draggable:".k-draggable-item"}},elements:function(){var t={cards:{list:"k-cards",item:"k-card"},list:{list:"k-list",item:"k-list-item"}};return t[this.layout]?t[this.layout]:t["list"]},paginationOptions:function(){var t="object"!==Object(Ht["a"])(this.pagination)?{}:this.pagination;return Object(I["a"])({limit:10,details:!0,keys:!1,total:0,hide:!1},t)}},watch:{$props:function(){this.$forceUpdate()}},over:null,methods:{onEnd:function(){this.over&&this.over.removeAttribute("data-over"),this.$emit("sort",this.items)},onDragStart:function(t,e){this.$store.dispatch("drag",{type:"text",data:e})}}},ac=sc,oc=(n("8c28"),Object(_["a"])(ac,nc,ic,!1,null,null,null)),rc=oc.exports,lc=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-column",attrs:{"data-width":t.width}},[t._t("default")],2)},uc=[],cc={props:{width:String}},dc=cc,pc=(n("c9cb"),Object(_["a"])(dc,lc,uc,!1,null,null,null)),fc=pc.exports,hc=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("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)},mc=[],gc={props:{label:{type:String,default:"Drop to upload"},disabled:{type:Boolean,default:!1}},data:function(){return{files:[],dragging:!1,over:!1}},methods:{cancel:function(){this.reset()},reset:function(){this.dragging=!1,this.over=!1},onDrop:function(t){return!0===this.disabled?this.reset():!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:function(t){!1===this.disabled&&this.$helper.isUploadEvent(t)&&(this.dragging=!0)},onLeave:function(){this.reset()},onOver:function(t){!1===this.disabled&&this.$helper.isUploadEvent(t)&&(t.dataTransfer.dropEffect="copy",this.over=!0)}}},bc=gc,vc=(n("414d"),Object(_["a"])(bc,hc,mc,!1,null,null,null)),kc=vc.exports,$c=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",t._g({staticClass:"k-empty",attrs:{"data-layout":t.layout}},t.$listeners),[t.icon?n("k-icon",{attrs:{type:t.icon}}):t._e(),n("p",[t._t("default")],2)],1)},_c=[],yc={props:{text:String,icon:String,layout:{type:String,default:"list"}}},xc=yc,wc=(n("ba8f"),Object(_["a"])(xc,$c,_c,!1,null,null,null)),Oc=wc.exports,Cc=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.file.url,title:t.$t("open"),target:"_blank"}},[t.file.panelImage&&t.file.panelImage.cards&&t.file.panelImage.cards.url?n("k-image",{attrs:{src:t.file.panelImage.cards.url,srcset:t.file.panelImage.cards.srcset,back:"none"}}):t.file.panelIcon?n("k-icon",{staticClass:"k-file-preview-icon",style:{color:t.file.panelIcon.color},attrs:{type:t.file.panelIcon.type}}):n("span",{staticClass:"k-file-preview-placeholder"})],1)],1),n("div",{staticClass:"k-file-preview-details"},[n("ul",[n("li",[n("h3",[t._v(t._s(t.$t("template")))]),n("p",[t._v(t._s(t.file.template||"—"))])]),n("li",[n("h3",[t._v(t._s(t.$t("mime")))]),n("p",[t._v(t._s(t.file.mime))])]),n("li",[n("h3",[t._v(t._s(t.$t("url")))]),n("p",[n("k-link",{attrs:{to:t.file.url,tabindex:"-1",target:"_blank"}},[t._v("/"+t._s(t.file.id))])],1)]),n("li",[n("h3",[t._v(t._s(t.$t("size")))]),n("p",[t._v(t._s(t.file.niceSize))])]),n("li",[n("h3",[t._v(t._s(t.$t("dimensions")))]),t.file.dimensions?n("p",[t._v(t._s(t.file.dimensions.width)+"×"+t._s(t.file.dimensions.height)+" "+t._s(t.$t("pixel")))]):n("p",[t._v("—")])]),n("li",[n("h3",[t._v(t._s(t.$t("orientation")))]),t.file.dimensions?n("p",[t._v(t._s(t.$t("orientation."+t.file.dimensions.orientation)))]):n("p",[t._v("—")])])])])])],1)},Sc=[],Ec={props:{file:Object}},jc=Ec,Tc=(n("696b5"),Object(_["a"])(jc,Cc,Sc,!1,null,null,null)),Ic=Tc.exports,Lc=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-grid",attrs:{"data-gutter":t.gutter}},[t._t("default")],2)},qc=[],Ac={props:{gutter:String}},Nc=Ac,Bc=(n("5b23"),Object(_["a"])(Nc,Lc,qc,!1,null,null,null)),Pc=Bc.exports,Dc=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"},[t._t("left",null,{slot:"left"}),t._t("right",null,{slot:"right"})],2):t._e(),t.tabs&&t.tabs.length>1?n("div",{staticClass:"k-header-tabs"},[n("nav",[t._l(t.visibleTabs,function(e,i){return n("k-button",{key:t.$route.fullPath+"-tab-"+i,staticClass:"k-tab-button",attrs:{link:"#"+e.name,current:t.currentTab&&t.currentTab.name===e.name,icon:e.icon,tooltip:e.label}},[t._v("\n "+t._s(e.label)+"\n ")])}),t.invisibleTabs.length?n("k-button",{staticClass:"k-tab-button k-tabs-dropdown-button",attrs:{icon:"dots"},on:{click:function(e){return e.stopPropagation(),t.$refs.more.toggle()}}},[t._v("\n "+t._s(t.$t("more"))+"\n ")]):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,i){return n("k-dropdown-item",{key:"more-"+i,attrs:{link:"#"+e.name,current:t.currentTab&&t.currentTab.name===e.name,icon:e.icon,tooltip:e.label}},[t._v("\n "+t._s(e.label)+"\n ")])}),1):t._e()],1):t._e()],1)},Rc=[],Mc={props:{editable:Boolean,tabs:Array,tab:Object},data:function(){return{size:null,currentTab:this.tab,visibleTabs:this.tabs,invisibleTabs:[]}},watch:{tab:function(){this.currentTab=this.tab},tabs:function(t){this.visibleTabs=t,this.invisibleTabs=[],this.resize(!0)}},created:function(){window.addEventListener("resize",this.resize)},destroyed:function(){window.removeEventListener("resize",this.resize)},methods:{resize:function(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"}}}}},zc=Mc,Uc=(n("53c5"),Object(_["a"])(zc,Dc,Rc,!1,null,null,null)),Fc=Uc.exports,Hc=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("ul",{staticClass:"k-list"},[t._t("default",t._l(t.items,function(e,i){return n("k-list-item",t._g(t._b({key:i},"k-list-item",e,!1),t.$listeners))}))],2)},Kc=[],Vc={props:{items:Array}},Yc=Vc,Wc=(n("c857"),Object(_["a"])(Yc,Hc,Kc,!1,null,null,null)),Gc=Wc.exports,Jc=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n(t.element,t._g({tag:"component",staticClass:"k-list-item"},t.$listeners),[t.sortable?n("k-sort-handle"):t._e(),n("k-link",{staticClass:"k-list-item-content",attrs:{to:t.link,target:t.target}},[n("span",{staticClass:"k-list-item-image"},[t.imageOptions?n("k-image",t._b({},"k-image",t.imageOptions,!1)):n("k-icon",t._b({},"k-icon",t.icon,!1))],1),n("span",{staticClass:"k-list-item-text"},[n("em",[t._v(t._s(t.text))]),t.info?n("small",{domProps:{innerHTML:t._s(t.info)}}):t._e()])]),n("nav",{staticClass:"k-list-item-options"},[t._t("options",[t.flag?n("k-button",t._b({staticClass:"k-list-item-status",on:{click:t.flag.click}},"k-button",t.flag,!1)):t._e(),t.options?n("k-button",{staticClass:"k-list-item-toggle",attrs:{tooltip:t.$t("options"),icon:"dots",alt:"Options"},on:{click:function(e){return e.stopPropagation(),t.$refs.options.toggle()}}}):t._e(),n("k-dropdown-content",{ref:"options",attrs:{options:t.options,align:"right"},on:{action:function(e){return t.$emit("action",e)}}})])],2)],1)},Zc=[],Xc={inheritAttrs:!1,props:{element:{type:String,default:"li"},image:Object,icon:{type:Object,default:function(){return{type:"file",back:"black"}}},sortable:Boolean,text:String,target:String,info:String,link:[String,Function],flag:Object,options:[Array,Function]},computed:{imageOptions:function(){return tu(this.image)}}},Qc=Xc,td=(n("fa6a"),Object(_["a"])(Qc,Jc,Zc,!1,null,null,null)),ed=td.exports,nd=function(){var t=this,e=t.$createElement,n=t._self._c||e;return 0===t.tabs.length?n("k-box",{attrs:{text:"This page has no blueprint setup yet",theme:"info"}}):t.tab?n("k-sections",{attrs:{parent:t.parent,blueprint:t.blueprint,columns:t.tab.columns},on:{submit:function(e){return t.$emit("submit",e)}}}):t._e()},id=[],sd={props:{parent:String,blueprint:String,tabs:Array},data:function(){return{tab:null}},watch:{$route:function(){this.open()},blueprint:function(){this.open()}},mounted:function(){this.open()},methods:{open:function(t){if(0!==this.tabs.length){t||(t=this.$route.hash.replace("#","")),t||(t=this.tabs[0].name);var e=null;this.tabs.forEach(function(n){n.name===t&&(e=n)}),e||(e=this.tabs[0]),this.tab=e,this.$emit("tab",this.tab)}}}},ad=sd,od=Object(_["a"])(ad,nd,id,!1,null,null,null),rd=od.exports,ld=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-view",attrs:{"data-align":t.align}},[t._t("default")],2)},ud=[],cd={props:{align:String}},dd=cd,pd=(n("daa8"),Object(_["a"])(dd,ld,ud,!1,null,null,null)),fd=pd.exports,hd=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("draggable",t._g(t._b({staticClass:"k-draggable",attrs:{tag:t.element,list:t.list,move:t.move}},"draggable",t.dragOptions,!1),t.listeners),[t._t("default"),t._t("footer",null,{slot:"footer"})],2)},md=[],gd=n("1980"),bd=n.n(gd),vd={components:{draggable:bd.a},props:{element:String,handle:[String,Boolean],list:[Array,Object],move:Function,options:Object},data:function(){var t=this;return{listeners:Object(I["a"])({},this.$listeners,{start:function(e){t.$store.dispatch("drag",{}),t.$listeners.start&&t.$listeners.start(e)},end:function(e){t.$store.dispatch("drag",null),t.$listeners.end&&t.$listeners.end(e)}})}},computed:{dragOptions:function(){var t=!1;return t=!0===this.handle?".k-sort-handle":this.handle,Object(I["a"])({fallbackClass:"k-sortable-fallback",fallbackOnBody:!0,forceFallback:!0,ghostClass:"k-sortable-ghost",handle:t,scroll:document.querySelector(".k-panel-view")},this.options)}}},kd=vd,$d=Object(_["a"])(kd,hd,md,!1,null,null,null),_d=$d.exports,yd={data:function(){return{error:null}},errorCaptured:function(t){return A.debug&&window.console.warn(t),this.error=t,!1},render:function(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]}},xd=yd,wd=Object(_["a"])(xd,qr,Ar,!1,null,null,null),Od=wd.exports,Cd=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)},Sd=[],Ed={props:{link:String,size:{type:String},tag:{type:String,default:"h2"},theme:{type:String}}},jd=Ed,Td=(n("f8a7"),Object(_["a"])(jd,Cd,Sd,!1,null,null,null)),Id=Td.exports,Ld=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("span",{class:"k-icon k-icon-"+t.type,attrs:{"aria-label":t.alt,role:t.alt?"img":null,"aria-hidden":!t.alt,"data-back":t.back,"data-size":t.size}},[t.emoji?n("span",{staticClass:"k-icon-emoji"},[t._v(t._s(t.type))]):n("svg",{style:{color:t.color},attrs:{viewBox:"0 0 16 16"}},[n("use",{attrs:{"xlink:href":"#icon-"+t.type}})])])},qd=[],Ad={props:{alt:String,color:String,back:String,emoji:Boolean,size:String,type:String}},Nd=Ad,Bd=(n("3342"),Object(_["a"])(Nd,Ld,qd,!1,null,null,null)),Pd=Bd.exports,Dd=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)])},Rd=[],Md={props:{alt:String,back:String,cover:Boolean,ratio:String,sizes:String,src:String,srcset:String},data:function(){return{loaded:{type:Boolean,default:!1},error:{type:Boolean,default:!1}}},computed:{ratioPadding:function(){return this.$helper.ratio(this.ratio||"1/1")}},created:function(){var t=this,e=new Image;e.onload=function(){t.loaded=!0,t.$emit("load")},e.onerror=function(){t.error=!0,t.$emit("error")},e.src=this.src}},zd=Md,Ud=(n("0d56"),Object(_["a"])(zd,Dd,Rd,!1,null,null,null)),Fd=Ud.exports,Hd=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("progress",{staticClass:"k-progress",attrs:{max:"100"},domProps:{value:t.state}},[t._v("\n "+t._s(t.state)+"%\n")])},Kd=[],Vd={props:{value:{type:Number,default:0}},data:function(){return{state:this.value}},methods:{set:function(t){this.state=t}}},Yd=Vd,Wd=(n("9799"),Object(_["a"])(Yd,Hd,Kd,!1,null,null,null)),Gd=Wd.exports,Jd=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("span",{staticClass:"k-sort-handle",attrs:{"aria-hidden":"true"}},[n("svg",{attrs:{viewBox:"0 0 16 16"}},[n("use",{attrs:{"xlink:href":"#icon-sort"}})])])},Zd=[],Xd=(n("35cb"),{}),Qd=Object(_["a"])(Xd,Jd,Zd,!1,null,null,null),tp=Qd.exports,ep=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-text",attrs:{"data-align":t.align,"data-size":t.size,"data-theme":t.theme}},[t._t("default")],2)},np=[],ip={props:{align:String,size:String,theme:String}},sp=ip,ap=(n("b0d6"),Object(_["a"])(sp,ep,np,!1,null,null,null)),op=ap.exports,rp=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n(t.component,t._g(t._b({ref:"button",tag:"component"},"component",t.$props,!1),t.$listeners),[t._t("default")],2)},lp=[],up={inheritAttrs:!1,props:{autofocus:Boolean,current:[String,Boolean],disabled:Boolean,icon:String,id:[String,Number],link:String,responsive:Boolean,rel:String,role:String,target:String,tabindex:String,theme:String,tooltip:String,type:{type:String,default:"button"}},computed:{component:function(){return!0===this.disabled?"k-button-disabled":this.link?"k-button-link":"k-button-native"}},methods:{focus:function(){this.$refs.button.focus&&this.$refs.button.focus()},tab:function(){this.$refs.button.tab&&this.$refs.button.tab()},untab:function(){this.$refs.button.untab&&this.$refs.button.untab()}}},cp=up,dp=(n("3787"),Object(_["a"])(cp,rp,lp,!1,null,null,null)),pp=dp.exports,fp=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)},hp=[],mp={inheritAttrs:!1,props:{icon:String,id:[String,Number],responsive:Boolean,theme:String,tooltip:String}},gp=mp,bp=(n("16eb"),Object(_["a"])(gp,fp,hp,!1,null,null,null)),vp=bp.exports,kp=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-button-group"},[t._t("default")],2)},$p=[],_p=(n("a567"),{}),yp=Object(_["a"])(_p,kp,$p,!1,null,null,null),xp=yp.exports,wp=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-link",t._g({staticClass:"k-button",attrs:{"aria-current":t.current,autofocus:t.autofocus,id:t.id,"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)},Op=[],Cp={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}},Sp=Cp,Ep=Object(_["a"])(Sp,wp,Op,!1,null,null,null),jp=Ep.exports,Tp=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("button",t._g({staticClass:"k-button",attrs:{"aria-current":t.current,autofocus:t.autofocus,id:t.id,"data-theme":t.theme,"data-responsive":t.responsive,role:t.role,tabindex:t.tabindex,title:t.tooltip,type:t.type}},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)},Ip=[],Lp={mounted:function(){this.$el.addEventListener("keyup",this.onTab,!0),this.$el.addEventListener("blur",this.onUntab,!0)},destroyed:function(){this.$el.removeEventListener("keyup",this.onTab,!0),this.$el.removeEventListener("blur",this.onUntab,!0)},methods:{focus:function(){this.$el.focus&&this.$el.focus()},onTab:function(t){9===t.keyCode&&this.$el.setAttribute("data-tabbed",!0)},onUntab:function(){this.$el.removeAttribute("data-tabbed")},tab:function(){this.$el.focus(),this.$el.setAttribute("data-tabbed",!0)},untab:function(){this.$el.removeAttribute("data-tabbed")}}},qp={mixins:[Lp],inheritAttrs:!1,props:{autofocus:Boolean,current:[String,Boolean],icon:String,id:[String,Number],responsive:Boolean,role:String,tabindex:String,theme:String,tooltip:String,type:{type:String,default:"button"}}},Ap=qp,Np=Object(_["a"])(Ap,Tp,Ip,!1,null,null,null),Bp=Np.exports,Pp=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("span",{staticClass:"k-dropdown",on:{click:function(t){t.stopPropagation()}}},[t._t("default")],2)},Dp=[],Rp=(n("f95f"),{}),Mp=Object(_["a"])(Rp,Pp,Dp,!1,null,null,null),zp=Mp.exports,Up=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}},[t._t("default",[t._l(t.items,function(e,i){return["-"===e?n("hr",{key:t._uid+"-item-"+i}):n("k-dropdown-item",t._b({key:t._uid+"-item-"+i,ref:t._uid+"-item-"+i,refInFor:!0,on:{click:function(n){return t.$emit("action",e.click)}}},"k-dropdown-item",e,!1),[t._v("\n "+t._s(e.text)+"\n ")])]})])],2):t._e()},Fp=[],Hp=null,Kp={props:{options:[Array,Function],align:String},data:function(){return{items:[],current:-1,isOpen:!1}},methods:{fetchOptions:function(t){if(!this.options)return t(this.items);"string"===typeof this.options?fetch(this.options).then(function(t){return t.json()}).then(function(e){return t(e)}):"function"===typeof this.options?this.options(t):xt()(this.options)&&t(this.options)},open:function(){var t=this;this.reset(),Hp&&Hp!==this&&Hp.close(),this.fetchOptions(function(e){t.$events.$on("keydown",t.navigate),t.$events.$on("click",t.close),t.items=e,t.isOpen=!0,t.$emit("open"),Hp=t})},reset:function(){this.current=-1,this.$events.$off("keydown",this.navigate),this.$events.$off("click",this.close)},close:function(){this.reset(),this.isOpen=Hp=!1,this.$emit("close")},toggle:function(){this.isOpen?this.close():this.open()},focus:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0;this.$children[t]&&this.$children[t].focus&&(this.current=t,this.$children[t].focus())},navigate:function(t){switch(t.code){case"Escape":case"ArrowLeft":this.close(),this.$emit("leave",t.code);break;case"ArrowUp":t.preventDefault();while(1){if(this.current--,this.current<0){this.close(),this.$emit("leave",t.code);break}if(this.$children[this.current]&&!1===this.$children[this.current].disabled){this.focus(this.current);break}}break;case"ArrowDown":t.preventDefault();while(1){if(this.current++,this.current>this.$children.length-1){var e=this.$children.filter(function(t){return!1===t.disabled});this.current=this.$children.indexOf(e[e.length-1]);break}if(this.$children[this.current]&&!1===this.$children[this.current].disabled){this.focus(this.current);break}}break;case"Tab":while(1){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}break}}}},Vp=Kp,Yp=(n("98a1"),Object(_["a"])(Vp,Up,Fp,!1,null,null,null)),Wp=Yp.exports,Gp=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-button",t._g(t._b({ref:"button",staticClass:"k-dropdown-item"},"k-button",t.$props,!1),t.listeners),[t._t("default")],2)},Jp=[],Zp={inheritAttrs:!1,props:{disabled:Boolean,icon:String,image:[String,Object],link:String,target:String,theme:String,upload:String,current:[String,Boolean]},data:function(){var t=this;return{listeners:Object(I["a"])({},this.$listeners,{click:function(e){t.$parent.close(),t.$emit("click",e)}})}},methods:{focus:function(){this.$refs.button.focus()},tab:function(){this.$refs.button.tab()}}},Xp=Zp,Qp=(n("580a"),Object(_["a"])(Xp,Gp,Jp,!1,null,null,null)),tf=Qp.exports,ef=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)},nf=[],sf={mixins:[Lp],props:{disabled:Boolean,rel:String,tabindex:[String,Number],target:String,title:String,to:[String,Function]},data:function(){return{relAttr:"_blank"===this.target?"noreferrer noopener":this.rel,listeners:Object(I["a"])({},this.$listeners,{click:this.onClick})}},computed:{href:function(){return"function"===typeof this.to?"":void 0===this.$route||"/"!==this.to[0]||this.target?this.to:(this.$router.options.url||"")+this.to}},methods:{isRoutable:function(t){return void 0!==this.$route&&(!(t.metaKey||t.altKey||t.ctrlKey||t.shiftKey)&&(!t.defaultPrevented&&((void 0===t.button||0===t.button)&&!this.target)))},onClick:function(t){if(!0===this.disabled)return t.preventDefault(),!1;"function"===typeof this.to&&(t.preventDefault(),this.to()),this.isRoutable(t)&&(t.preventDefault(),this.$router.push(this.to)),this.$emit("click",t)}}},af=sf,of=(n("cc79"),Object(_["a"])(af,ef,nf,!1,null,null,null)),rf=of.exports,lf=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.languages.length?n("k-dropdown",[n("k-button",{attrs:{responsive:!0,icon:"globe"},on:{click:function(e){return t.$refs.languages.toggle()}}},[t._v("\n "+t._s(t.language.name)+"\n ")]),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("\n "+t._s(e.name)+"\n ")])})],2):t._e()],1):t._e()},uf=[],cf={computed:{defaultLanguage:function(){return this.$store.state.languages.default},language:function(){return this.$store.state.languages.current},languages:function(){return this.$store.state.languages.all.filter(function(t){return!1===t.default})}},methods:{change:function(t){this.$store.dispatch("languages/current",t),this.$emit("change",t)}}},df=cf,pf=Object(_["a"])(df,lf,uf,!1,null,null,null),ff=pf.exports,hf=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)+"\n ")],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("\n "+t._s(e)+"\n ")])}),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)+"\n ")],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()},mf=[],gf={props:{align:{type:String,default:"left"},details:{type:Boolean,default:!1},dropdown:{type:Boolean,default:!0},validate:{type:Function,default:function(){return Je.a.resolve()}},page:{type:Number,default:1},total:{type:Number,default:0},limit:{type:Number,default:10},keys:{type:Boolean,default:!1},pageLabel:{type:String,default:function(){return this.$t("pagination.page")}},prevLabel:{type:String,default:function(){return this.$t("prev")}},nextLabel:{type:String,default:function(){return this.$t("next")}}},data:function(){return{currentPage:this.page}},computed:{show:function(){return this.pages>1},start:function(){return(this.currentPage-1)*this.limit+1},end:function(){var t=this.start-1+this.limit;return t>this.total?this.total:t},detailsText:function(){return 1===this.limit?this.start+" / ":this.start+"-"+this.end+" / "},pages:function(){return Math.ceil(this.total/this.limit)},hasPrev:function(){return this.start>1},hasNext:function(){return this.endthis.limit},offset:function(){return this.start-1}},watch:{page:function(t){this.currentPage=ms()(t)}},created:function(){!0===this.keys&&window.addEventListener("keydown",this.navigate,!1)},destroyed:function(){window.removeEventListener("keydown",this.navigate,!1)},methods:{goTo:function(t){var e=this;this.validate(t).then(function(){t<1&&(t=1),t>e.pages&&(t=e.pages),e.currentPage=t,e.$refs.dropdown&&e.$refs.dropdown.close(),e.$emit("paginate",{page:e.currentPage,start:e.start,end:e.end,limit:e.limit,offset:e.offset})}).catch(function(){})},prev:function(){this.goTo(this.currentPage-1)},next:function(){this.goTo(this.currentPage+1)},navigate:function(t){switch(t.code){case"ArrowLeft":this.prev();break;case"ArrowRight":this.next();break}}}},bf=gf,vf=(n("a66d"),Object(_["a"])(bf,hf,mf,!1,null,null,null)),kf=vf.exports,$f=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-button-group",{staticClass:"k-prev-next"},[n("k-button",t._b({attrs:{icon:"angle-left"}},"k-button",t.prev,!1)),n("k-button",t._b({attrs:{icon:"angle-right"}},"k-button",t.next,!1))],1)},_f=[],yf={props:{prev:{type:Object,default:function(){return{disabled:!0,link:"#"}}},next:{type:Object,default:function(){return{disabled:!0,link:"#"}}}}},xf=yf,wf=(n("7a7d"),Object(_["a"])(xf,$f,_f,!1,null,null,null)),Of=wf.exports,Cf=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-search",attrs:{role:"search"},on:{click:t.close}},[n("div",{staticClass:"k-search-box",on:{click:function(t){t.stopPropagation()}}},[n("div",{staticClass:"k-search-input"},[n("k-dropdown",{staticClass:"k-search-types"},[n("k-button",{attrs:{icon:t.type.icon},on:{click:function(e){return t.$refs.types.toggle()}}},[t._v(t._s(t.type.label)+":")]),n("k-dropdown-content",{ref:"types"},t._l(t.types,function(e,i){return n("k-dropdown-item",{key:i,attrs:{icon:e.icon},on:{click:function(e){t.currentType=i}}},[t._v("\n "+t._s(e.label)+"\n ")])}),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('search')",type:"text"},domProps:{value:t.q},on:{keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"down",40,e.key,["Down","ArrowDown"])?null:(e.preventDefault(),t.down(e))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"up",38,e.key,["Up","ArrowUp"])?null:(e.preventDefault(),t.up(e))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"tab",9,e.key,"Tab")?null:(e.preventDefault(),t.tab(e))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:t.enter(e)},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"esc",27,e.key,["Esc","Escape"])?null:t.close(e)}],input:function(e){e.target.composing||(t.q=e.target.value)}}}),n("k-button",{staticClass:"k-search-close",attrs:{tooltip:t.$t("close"),icon:"cancel"},on:{click:t.close}})],1),n("ul",t._l(t.items,function(e,i){return n("li",{key:e.id,attrs:{"data-selected":t.selected===i},on:{mouseover:function(e){t.selected=i}}},[n("k-link",{attrs:{to:e.link},on:{click:function(e){return t.click(i)}}},[n("strong",[t._v(t._s(e.title))]),n("small",[t._v(t._s(e.info))])])],1)}),0)])])},Sf=[],Ef={data:function(){return{items:[],q:null,selected:-1,currentType:"users"===this.$store.state.view?"users":"pages"}},computed:{type:function(){return this.types[this.currentType]||this.types["pages"]},types:function(){return{pages:{label:this.$t("pages"),icon:"page",endpoint:"site/search"},users:{label:this.$t("users"),icon:"users",endpoint:"users/search"}}}},watch:{q:wt(function(t){this.search(t)},200),currentType:function(){this.search(this.q)}},mounted:function(){var t=this;this.$nextTick(function(){t.$refs.input.focus()})},methods:{open:function(t){t.preventDefault(),this.$store.dispatch("search",!0)},click:function(t){this.selected=t,this.tab()},close:function(){this.$store.dispatch("search",!1)},down:function(){this.selected=0&&this.selected--}}},jf=Ef,Tf=(n("4cb2"),Object(_["a"])(jf,Cf,Sf,!1,null,null,null)),If=Tf.exports,Lf=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("span",{ref:"button",staticClass:"k-tag",attrs:{"data-size":t.size,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(e))}}},[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()])},qf=[],Af={props:{removable:Boolean,size:String},methods:{remove:function(){this.removable&&this.$emit("remove")},focus:function(){this.$refs.button.focus()}}},Nf=Af,Bf=(n("021f"),Object(_["a"])(Nf,Lf,qf,!1,null,null,null)),Pf=Bf.exports,Df=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.user&&t.view?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"},[n("ul",[t._l(t.views,function(e,i){return e.menu?n("li",{key:"menu-item-"+i,attrs:{"aria-current":t.$store.state.view===i}},[n("k-dropdown-item",{attrs:{disabled:!1===t.$permissions.access[i],icon:e.icon,link:e.link}},[t._v("\n "+t._s(t.menuTitle(e,i))+"\n ")])],1):t._e()}),n("li",[n("hr")]),n("li",{attrs:{"aria-current":"account"===t.$route.meta.view}},[n("k-dropdown-item",{attrs:{icon:"account",link:"/account"}},[t._v("\n "+t._s(t.$t("view.account"))+"\n ")])],1),n("li",[n("hr")]),n("li",[n("k-dropdown-item",{attrs:{icon:"logout",link:"/logout"}},[t._v("\n "+t._s(t.$t("logout"))+"\n ")])],1)],2)])],1),t.view?n("k-link",{staticClass:"k-topbar-button k-topbar-view-button",attrs:{to:t.view.link}},[n("k-icon",{attrs:{type:t.view.icon}}),t._v(" "+t._s(t.breadcrumbTitle)+"\n ")],1):t._e(),t.$store.state.breadcrumb.length>1?n("k-dropdown",{staticClass:"k-topbar-breadcrumb-menu"},[n("k-button",{staticClass:"k-topbar-button",on:{click:function(e){return t.$refs.crumb.toggle()}}},[t._v("\n …\n "),n("k-icon",{attrs:{type:"angle-down"}})],1),n("k-dropdown-content",{ref:"crumb"},[n("k-dropdown-item",{attrs:{icon:t.view.icon,link:t.view.link}},[t._v("\n "+t._s(t.$t("view."+t.$store.state.view,t.view.label))+"\n ")]),t._l(t.$store.state.breadcrumb,function(e,i){return n("k-dropdown-item",{key:"crumb-"+i+"-dropdown",attrs:{icon:t.view.icon,link:e.link}},[t._v("\n "+t._s(e.label)+"\n ")])})],2)],1):t._e(),n("nav",{staticClass:"k-topbar-crumbs"},t._l(t.$store.state.breadcrumb,function(e,i){return n("k-link",{key:"crumb-"+i,attrs:{to:e.link}},[t._v("\n "+t._s(e.label)+"\n ")])}),1),n("div",{staticClass:"k-topbar-signals"},[n("span",{directives:[{name:"show",rawName:"v-show",value:t.$store.state.isLoading,expression:"$store.state.isLoading"}],staticClass:"k-topbar-loader"},[n("svg",{attrs:{viewBox:"0 0 16 18"}},[n("path",{attrs:{fill:"white",d:"M8,0 L16,4.50265232 L16,13.5112142 L8,18.0138665 L0,13.5112142 L0,4.50265232 L8,0 Z M2.10648757,5.69852516 L2.10648757,12.3153414 L8,15.632396 L13.8935124,12.3153414 L13.8935124,5.69852516 L8,2.38147048 L2.10648757,5.69852516 Z"}})])]),t.notification?[n("k-button",{staticClass:"k-topbar-notification k-topbar-signals-button",attrs:{theme:"positive"},on:{click:function(e){return t.$store.dispatch("notification/close")}}},[t._v("\n "+t._s(t.notification.message)+"\n ")])]:t.unregistered?[n("div",{staticClass:"k-registration"},[n("p",[t._v(t._s(t.$t("license.unregistered")))]),n("k-button",{staticClass:"k-topbar-signals-button",attrs:{responsive:!0,tooltip:t.$t("license.unregistered"),icon:"key"},on:{click:function(e){return t.$emit("register")}}},[t._v("\n "+t._s(t.$t("license.register"))+"\n ")]),n("k-button",{staticClass:"k-topbar-signals-button",attrs:{responsive:!0,link:"https://getkirby.com/buy",target:"_blank",icon:"cart"}},[t._v("\n "+t._s(t.$t("license.buy"))+"\n ")])],1)]:t._e(),[n("k-form-indicator")],n("k-button",{staticClass:"k-topbar-signals-button",attrs:{tooltip:t.$t("search"),icon:"search"},on:{click:function(e){return t.$store.dispatch("search",!0)}}})],2)],1)])],1):t._e()},Rf=[],Mf=Object(I["a"])({site:{link:"/site",icon:"page",menu:!0},users:{link:"/users",icon:"users",menu:!0},settings:{link:"/settings",icon:"settings",menu:!0},account:{link:"/account",icon:"users",menu:!1}},window.panel.plugins.views),zf={computed:{breadcrumbTitle:function(){var t=this.$t("view.".concat(this.$store.state.view),this.view.label);return"site"===this.$store.state.view&&this.$store.state.system.info.title||t},view:function(){return Mf[this.$store.state.view]},views:function(){return Mf},user:function(){return this.$store.state.user.current},notification:function(){return this.$store.state.notification.type&&"error"!==this.$store.state.notification.type?this.$store.state.notification:null},unregistered:function(){return!this.$store.state.system.info.license}},methods:{menuTitle:function(t,e){var n=this.$t("view."+e,t.label);return"site"===e&&this.$store.state.system.info.site||n}}},Uf=zf,Ff=(n("1e3b"),Object(_["a"])(Uf,Df,Rf,!1,null,null,null)),Hf=Ff.exports,Kf=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-grid",{staticClass:"k-sections",attrs:{gutter:"large"}},t._l(t.columns,function(e,i){return n("k-column",{key:t.parent+"-column-"+i,attrs:{width:e.width}},[t._l(e.sections,function(s,a){return t.meetsCondition(s)?[t.exists(s.type)?n("k-"+s.type+"-section",t._b({key:t.parent+"-column-"+i+"-section-"+a+"-"+t.blueprint,tag:"component",class:"k-section k-section-name-"+s.name,attrs:{name:s.name,parent:t.parent,blueprint:t.blueprint,column:e.width},on:{submit:function(e){return t.$emit("submit",e)}}},"component",s,!1)):[n("k-box",{key:t.parent+"-column-"+i+"-section-"+a,attrs:{text:t.$t("error.section.type.invalid",{type:s.type}),theme:"negative"}})]]:t._e()})],2)}),1)},Vf=[],Yf={props:{parent:String,blueprint:String,columns:[Array,Object]},computed:{content:function(){return this.$store.getters["content/values"]()}},methods:{exists:function(t){return z["a"].options.components["k-"+t+"-section"]},meetsCondition:function(t){var e=this;if(!t.when)return!0;var n=!0;return kt()(t.when).forEach(function(i){var s=e.content[i.toLowerCase()],a=t.when[i];s!==a&&(n=!1)}),n}}},Wf=Yf,Gf=(n("6bcd"),Object(_["a"])(Wf,Kf,Vf,!1,null,null,null)),Jf=Gf.exports,Zf=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)},Xf=[],Qf={props:{blueprint:String,help:String,name:String,parent:String},methods:{load:function(){return this.$api.get(this.parent+"/sections/"+this.name)}}},th={mixins:[Qf],data:function(){return{headline:null,issue:null,text:null,theme:null}},created:function(){var t=this;this.load().then(function(e){t.headline=e.options.headline,t.text=e.options.text,t.theme=e.options.theme||"info"}).catch(function(e){t.issue=e})}},eh=th,nh=(n("4333"),Object(_["a"])(eh,Zf,Xf,!1,null,null,null)),ih=nh.exports,sh=function(){var t=this,e=t.$createElement,n=t._self._c||e;return!1===t.isLoading?n("section",{staticClass:"k-pages-section"},[n("header",{staticClass:"k-section-header"},[n("k-headline",{attrs:{link:t.options.link}},[t._v("\n "+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",[n("k-button",{attrs:{icon:"add"},on:{click:t.create}},[t._v(t._s(t.$t("add")))])],1):t._e()],1),t.error?[n("k-box",{attrs:{theme:"negative"}},[n("k-text",{attrs:{size:"small"}},[n("strong",[t._v("\n "+t._s(t.$t("error.section.notLoaded",{name:t.name}))+":\n ")]),t._v("\n "+t._s(t.error)+"\n ")])],1)]:[t.data.length?n("k-collection",{attrs:{layout:t.options.layout,help:t.help,items:t.data,pagination:t.pagination,sortable:t.options.sortable,size:t.options.size,"data-invalid":t.isInvalid},on:{change:t.sort,paginate:t.paginate,action:t.action}}):[n("k-empty",{attrs:{layout:t.options.layout,"data-invalid":t.isInvalid,icon:"page"},on:{click:t.create}},[t._v("\n "+t._s(t.options.empty||t.$t("pages.empty"))+"\n ")]),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)],n("k-page-create-dialog",{ref:"create"}),n("k-page-duplicate-dialog",{ref:"duplicate"}),n("k-page-rename-dialog",{ref:"rename",on:{success:t.update}}),n("k-page-url-dialog",{ref:"url",on:{success:t.update}}),n("k-page-status-dialog",{ref:"status",on:{success:t.update}}),n("k-page-template-dialog",{ref:"template",on:{success:t.update}}),n("k-page-remove-dialog",{ref:"remove",on:{success:t.update}})]],2):t._e()},ah=[],oh={inheritAttrs:!1,props:{blueprint:String,column:String,parent:String,name:String},data:function(){return{data:[],error:null,isLoading:!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:function(){return this.options.headline||" "},help:function(){return this.options.help},isInvalid:function(){return!!(this.options.min&&this.data.lengththis.options.max)},language:function(){return this.$store.state.languages.current},paginationId:function(){return"kirby$pagination$"+this.parent+"/"+this.name}},watch:{language:function(){this.reload()}},methods:{items:function(t){return t},load:function(t){var e=this;t||(this.isLoading=!0),null===this.pagination.page&&(this.pagination.page=localStorage.getItem(this.paginationId)||1),this.$api.get(this.parent+"/sections/"+this.name,{page:this.pagination.page}).then(function(t){e.isLoading=!1,e.options=t.options,e.pagination=t.pagination,e.data=e.items(t.data)}).catch(function(t){e.isLoading=!1,e.error=t.message})},paginate:function(t){localStorage.setItem(this.paginationId,t.page),this.pagination=t,this.reload()},reload:function(){this.load(!0)}}},rh={mixins:[oh],computed:{add:function(){return this.options.add&&this.$permissions.pages.create}},created:function(){this.load(),this.$events.$on("page.changeStatus",this.reload)},destroyed:function(){this.$events.$off("page.changeStatus",this.reload)},methods:{create:function(){this.add&&this.$refs.create.open(this.options.link||this.parent,this.parent+"/children/blueprints",this.name)},action:function(t,e){var n=this;switch(e){case"duplicate":this.$refs.duplicate.open(t.id);break;case"preview":var i=window.open("","_blank");i.document.write="...",this.$api.pages.preview(t.id).then(function(t){i.location.href=t}).catch(function(t){n.$store.dispatch("notification/error",t)});break;case"rename":this.$refs.rename.open(t.id);break;case"url":this.$refs.url.open(t.id);break;case"status":this.$refs.status.open(t.id);break;case"template":this.$refs.template.open(t.id);break;case"remove":if(this.data.length<=this.options.min){var s=this.options.min>1?"plural":"singular";this.$store.dispatch("notification/error",{message:this.$t("error.section.pages.min."+s,{section:this.options.headline||this.name,min:this.options.min})});break}this.$refs.remove.open(t.id);break;default:throw new Error("Invalid action")}},items:function(t){var e=this;return t.map(function(t){var n=!1!==t.permissions.changeStatus;return t.flag={class:"k-status-flag k-status-flag-"+t.status,tooltip:n?e.$t("page.status"):"".concat(e.$t("page.status")," (").concat(e.$t("disabled"),")"),icon:n?"circle":"protected",disabled:!n,click:function(){e.action(t,"status")}},t.options=function(n){e.$api.pages.options(t.id,"list").then(function(t){return n(t)}).catch(function(t){e.$store.dispatch("notification/error",t)})},t.sortable=t.permissions.sort&&e.options.sortable,t.column=e.column,t})},sort:function(t){var e=this,n=null;if(t.added&&(n="added"),t.moved&&(n="moved"),n){var i=t[n].element,s=t[n].newIndex+1+this.pagination.offset;this.$api.pages.status(i.id,"listed",s).then(function(){e.$store.dispatch("notification/success",":)")}).catch(function(t){e.$store.dispatch("notification/error",{message:t.message,details:t.details}),e.reload()})}},update:function(){this.reload(),this.$events.$emit("model.update")}}},lh=rh,uh=Object(_["a"])(lh,sh,ah,!1,null,null,null),ch=uh.exports,dh=function(){var t=this,e=t.$createElement,n=t._self._c||e;return!1===t.isLoading?n("section",{staticClass:"k-files-section"},[n("header",{staticClass:"k-section-header"},[n("k-headline",[t._v("\n "+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",[n("k-button",{attrs:{icon:"upload"},on:{click:t.upload}},[t._v(t._s(t.$t("add")))])],1):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("\n "+t._s(t.error)+"\n ")])],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.options.sortable,size:t.options.size,"data-invalid":t.isInvalid},on:{sort:t.sort,paginate:t.paginate,action:t.action}}):[n("k-empty",{attrs:{layout:t.options.layout,"data-invalid":t.isInvalid,icon:"image"},on:{click:function(e){t.add&&t.upload()}}},[t._v("\n "+t._s(t.options.empty||t.$t("files.empty"))+"\n ")]),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-file-rename-dialog",{ref:"rename",on:{success:t.update}}),n("k-file-remove-dialog",{ref:"remove",on:{success:t.update}}),n("k-upload",{ref:"upload",on:{success:t.uploaded,error:t.reload}})]],2):t._e()},ph=[],fh={mixins:[oh],computed:{add:function(){return!(!this.$permissions.files.create||!1===this.options.upload)&&this.options.upload}},created:function(){this.load(),this.$events.$on("model.update",this.reload)},destroyed:function(){this.$events.$off("model.update",this.reload)},methods:{action:function(t,e){switch(e){case"edit":this.$router.push(t.link);break;case"download":window.open(t.url);break;case"rename":this.$refs.rename.open(t.parent,t.filename);break;case"replace":this.$refs.upload.open({url:A.api+"/"+this.$api.files.url(t.parent,t.filename),accept:"."+t.extension+","+t.mime,multiple:!1});break;case"remove":if(this.data.length<=this.options.min){var n=this.options.min>1?"plural":"singular";this.$store.dispatch("notification/error",{message:this.$t("error.section.files.min."+n,{section:this.options.headline||this.name,min:this.options.min})});break}this.$refs.remove.open(t.parent,t.filename);break}},drop:function(t){if(!1===this.add)return!1;this.$refs.upload.drop(t,Object(I["a"])({},this.add,{url:A.api+"/"+this.add.api}))},items:function(t){var e=this;return t.map(function(t){return t.options=function(n){e.$api.files.options(t.parent,t.filename,"list").then(function(t){return n(t)}).catch(function(t){e.$store.dispatch("notification/error",t)})},t.sortable=e.options.sortable,t.column=e.column,t})},replace:function(t){this.$refs.upload.open({url:A.api+"/"+this.$api.files.url(t.parent,t.filename),accept:t.mime,multiple:!1})},sort:function(t){var e=this;if(!1===this.options.sortable)return!1;t=t.map(function(t){return t.id}),this.$api.patch(this.parent+"/files/sort",{files:t,index:this.pagination.offset}).then(function(){e.$store.dispatch("notification/success",":)")}).catch(function(t){e.reload(),e.$store.dispatch("notification/error",t.message)})},update:function(){this.$events.$emit("model.update")},upload:function(){if(!1===this.add)return!1;this.$refs.upload.open(Object(I["a"])({},this.add,{url:A.api+"/"+this.add.api}))},uploaded:function(){this.$events.$emit("file.create"),this.$events.$emit("model.update"),this.$store.dispatch("notification/success",":)")}}},hh=fh,mh=Object(_["a"])(hh,dh,ph,!1,null,null,null),gh=mh.exports,bh=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,theme:"negative"}})]:t._e(),n("k-form",{attrs:{fields:t.fields,validate:!0,value:t.values,disabled:null!==t.$store.state.content.status.lock},on:{input:t.input,submit:t.onSubmit}})],2)},vh=[],kh={mixins:[Qf],inheritAttrs:!1,data:function(){return{fields:{},isLoading:!0,issue:null}},computed:{language:function(){return this.$store.state.languages.current},values:function(){return this.$store.getters["content/values"]()}},watch:{language:function(){this.fetch()}},created:function(){this.fetch()},methods:{input:function(t,e,n){this.$store.dispatch("content/update",[n,t[n]])},fetch:function(){var t=this;this.$api.get(this.parent+"/sections/"+this.name).then(function(e){t.fields=e.fields,kt()(t.fields).forEach(function(e){t.fields[e].section=t.name,t.fields[e].endpoints={field:t.parent+"/fields/"+e,section:t.parent+"/sections/"+t.name,model:t.parent}}),t.isLoading=!1}).catch(function(e){t.issue=e,t.isLoading=!1})},onSubmit:function(t){this.$events.$emit("keydown.cmd.s",t)}}},$h=kh,_h=(n("7d5d"),Object(_["a"])($h,bh,vh,!1,null,null,null)),yh=_h.exports,xh=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-error-view",{staticClass:"k-browser-view"},[n("p",[t._v("\n We are really sorry, but your browser does not support\n all features required for the Kirby Panel.\n ")]),!1===t.hasFetchSupport?[n("p",[n("strong",[t._v("Fetch")]),n("br"),t._v("\n We use Javascript's new Fetch API. You can find a list of supported browsers for this feature on\n "),n("strong",[n("a",{attrs:{href:"https://caniuse.com/#feat=fetch"}},[t._v("caniuse.com")])])])]:t._e(),!1===t.hasGridSupport?[n("p",[n("strong",[t._v("CSS Grid")]),n("br"),t._v("\n We use CSS Grids for all our layouts. You can find a list of supported browsers for this feature on\n "),n("strong",[n("a",{attrs:{href:"https://caniuse.com/#feat=css-grid"}},[t._v("caniuse.com")])])])]:t._e()],2)},wh=[],Oh={grid:function(){return!(!window.CSS||!window.CSS.supports("display","grid"))},fetch:function(){return void 0!==window.fetch},all:function(){return this.fetch()&&this.grid()}},Ch={computed:{hasFetchSupport:function(){return Oh.fetch()},hasGridSupport:function(){return Oh.grid()}},created:function(){this.$store.dispatch("content/current",null),Oh.all()&&this.$router.push("/")}},Sh=Ch,Eh=(n("d6fc"),Object(_["a"])(Sh,xh,wh,!1,null,null,null)),jh=Eh.exports,Th=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-error-boundary",{key:t.plugin,scopedSlots:t._u([{key:"error",fn:function(e){var i=e.error;return n("k-error-view",{},[t._v("\n "+t._s(i.message||i)+"\n ")])}}])},[n("k-"+t.plugin+"-plugin-view",{tag:"component"})],1)},Ih=[],Lh={props:{plugin:String},beforeRouteEnter:function(t,e,n){n(function(t){t.$store.dispatch("breadcrumb",[]),t.$store.dispatch("content/current",null)})},watch:{plugin:{handler:function(){this.$store.dispatch("view",this.plugin)},immediate:!0}}},qh=Lh,Ah=Object(_["a"])(qh,Th,Ih,!1,null,null,null),Nh=Ah.exports,Bh=function(){var t=this,e=t.$createElement,n=t._self._c||e;return 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),n("p",[t._t("default")],2)])],1)])},Ph=[],Dh=(n("d221"),{}),Rh=Object(_["a"])(Dh,Bh,Ph,!1,null,null,null),Mh=Rh.exports,zh=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.issue?n("k-error-view",[t._v("\n "+t._s(t.issue.message)+"\n")]):n("div",{staticClass:"k-file-view"},[n("k-file-preview",{attrs:{file:t.file}}),n("k-view",{staticClass:"k-file-content",attrs:{"data-locked":t.isLocked}},[n("k-header",{attrs:{editable:t.permissions.changeName&&!t.isLocked,tabs:t.tabs,tab:t.tab},on:{edit:function(e){return t.action("rename")}}},[t._v("\n\n "+t._s(t.file.filename)+"\n\n "),n("k-button-group",{attrs:{slot:"left"},slot:"left"},[n("k-button",{attrs:{responsive:!0,icon:"open"},on:{click:function(e){return t.action("download")}}},[t._v("\n "+t._s(t.$t("open"))+"\n ")]),n("k-dropdown",[n("k-button",{attrs:{responsive:!0,disabled:t.isLocked,icon:"cog"},on:{click:function(e){return t.$refs.settings.toggle()}}},[t._v("\n "+t._s(t.$t("settings"))+"\n ")]),n("k-dropdown-content",{ref:"settings",attrs:{options:t.options},on:{action:t.action}})],1),n("k-languages-dropdown")],1),t.file.id?n("k-prev-next",{attrs:{slot:"right",prev:t.prev,next:t.next},slot:"right"}):t._e()],1),t.file.id?n("k-tabs",{key:t.tabsKey,ref:"tabs",attrs:{parent:t.$api.files.url(t.path,t.file.filename),tabs:t.tabs,blueprint:t.file.blueprint.name},on:{tab:function(e){t.tab=e}}}):t._e(),n("k-file-rename-dialog",{ref:"rename",on:{success:t.renamed}}),n("k-file-remove-dialog",{ref:"remove",on:{success:t.deleted}}),n("k-upload",{ref:"upload",attrs:{url:t.uploadApi,accept:t.file.mime,multiple:!1},on:{success:t.uploaded}})],1)],1)},Uh=[],Fh={computed:{isLocked:function(){return null!==this.$store.state.content.status.lock}},created:function(){this.fetch(),this.$events.$on("model.reload",this.fetch),this.$events.$on("keydown.left",this.toPrev),this.$events.$on("keydown.right",this.toNext)},destroyed:function(){this.$events.$off("model.reload",this.fetch),this.$events.$off("keydown.left",this.toPrev),this.$events.$off("keydown.right",this.toNext)},methods:{toPrev:function(t){this.prev&&"body"===t.target.localName&&this.$router.push(this.prev.link)},toNext:function(t){this.next&&"body"===t.target.localName&&this.$router.push(this.next.link)}}},Hh={mixins:[Fh],props:{path:{type:String},filename:{type:String,required:!0}},data:function(){return{name:"",file:{id:null,parent:null,filename:"",url:"",prev:null,next:null,panelIcon:null,panelImage:null,mime:null,content:{}},permissions:{changeName:!1,delete:!1},issue:null,tabs:[],tab:null,options:null}},computed:{uploadApi:function(){return A.api+"/"+this.path+"/files/"+this.filename},prev:function(){if(this.file.prev)return{link:this.$api.files.link(this.path,this.file.prev.filename),tooltip:this.file.prev.filename}},tabsKey:function(){return"file-"+this.file.id+"-tabs"},language:function(){return this.$store.state.languages.current},next:function(){if(this.file.next)return{link:this.$api.files.link(this.path,this.file.next.filename),tooltip:this.file.next.filename}}},watch:{language:function(){this.fetch()},filename:function(){this.fetch()}},methods:{fetch:function(){var t=this;this.$api.files.get(this.path,this.filename,{view:"panel"}).then(function(e){t.file=e,t.file.next=e.nextWithTemplate,t.file.prev=e.prevWithTemplate,t.file.url=e.url,t.name=e.name,t.tabs=e.blueprint.tabs,t.permissions=e.options,t.options=function(e){t.$api.files.options(t.path,t.file.filename).then(function(t){e(t)})},t.$store.dispatch("breadcrumb",t.$api.files.breadcrumb(t.file,t.$route.name)),t.$store.dispatch("title",t.filename),t.$store.dispatch("content/create",{id:"files/"+e.id,api:t.$api.files.link(t.path,t.filename),content:e.content})}).catch(function(e){window.console.error(e),t.issue=e})},action:function(t){switch(t){case"download":window.open(this.file.url);break;case"rename":this.$refs.rename.open(this.path,this.file.filename);break;case"replace":this.$refs.upload.open({url:A.api+"/"+this.$api.files.url(this.path,this.file.filename),accept:this.file.mime});break;case"remove":this.$refs.remove.open(this.path,this.file.filename);break}},deleted:function(){this.path?this.$router.push("/"+this.path):this.$router.push("/site")},renamed:function(t){this.$router.push(this.$api.files.link(this.path,t.filename))},uploaded:function(){this.fetch(),this.$store.dispatch("notification/success",":)")}}},Kh=Hh,Vh=Object(_["a"])(Kh,zh,Uh,!1,null,null,null),Yh=Vh.exports,Wh=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.system?n("k-view",{staticClass:"k-installation-view",attrs:{align:"center"}},["install"===t.state?n("form",{on:{submit:function(e){return e.preventDefault(),t.install(e)}}},[n("h1",{staticClass:"k-offscreen"},[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:{type:"submit",icon:"check"}},[t._v(t._s(t.$t("install")))])],1):"completed"===t.state?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):n("div",[t.system.isInstalled?t._e():n("k-headline",[t._v(t._s(t.$t("installation.issues.headline")))]),n("ul",{staticClass:"k-installation-issues"},[!1===t.system.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:{icon:"refresh"},on:{click:t.check}},[n("span",{domProps:{innerHTML:t._s(t.$t("retry"))}})])],1)],1):t._e()},Gh=[],Jh={data:function(){return{user:{name:"",email:"",language:"",password:"",role:"admin"},languages:[],system:null}},computed:{state:function(){return this.system.isOk&&this.system.isInstallable&&!this.system.isInstalled?"install":this.system.isOk&&this.system.isInstallable&&this.system.isInstalled?"completed":void 0},translation:function(){return this.$store.state.translation.current},requirements:function(){return this.system&&this.system.requirements?this.system.requirements:{}},fields:function(){return{email:{label:this.$t("email"),type:"email",link:!1,required:!0},password:{label:this.$t("password"),type:"password",placeholder:this.$t("password")+" …",required:!0},language:{label:this.$t("language"),type:"select",options:this.languages,icon:"globe",empty:!1,required:!0}}}},watch:{translation:{handler:function(t){this.user.language=t},immediate:!0},"user.language":function(t){this.$store.dispatch("translation/activate",t)}},created:function(){this.$store.dispatch("content/current",null),this.check()},methods:{install:function(){var t=this;this.$api.system.install(this.user).then(function(e){t.$store.dispatch("user/current",e),t.$store.dispatch("notification/success",t.$t("welcome")+"!"),t.$router.push("/")}).catch(function(e){t.$store.dispatch("notification/error",e)})},check:function(){var t=this;this.$store.dispatch("system/load",!0).then(function(e){!0===e.isInstalled&&e.isReady?t.$router.push("/login"):t.$api.translations.options().then(function(n){t.languages=n,t.system=e,t.$store.dispatch("title",t.$t("view.installation"))})})}}},Zh=Jh,Xh=(n("146c"),Object(_["a"])(Zh,Wh,Gh,!1,null,null,null)),Qh=Xh.exports,tm=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.issue?n("k-error-view",[t._v("\n "+t._s(t.issue.message)+"\n")]):t.ready?n("k-view",{staticClass:"k-login-view",attrs:{align:"center"}},[n("k-login-form")],1):t._e()},em=[],nm=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(e)}}},[n("h1",{staticClass:"k-offscreen"},[t._v(t._s(t.$t("login")))]),t.issue?n("div",{staticClass:"k-login-alert",on:{click:function(e){t.issue=null}}},[n("span",[t._v(t._s(t.issue))]),n("k-icon",{attrs:{type:"alert"}})],1):t._e(),n("k-fieldset",{attrs:{novalidate:!0,fields:t.fields},model:{value:t.user,callback:function(e){t.user=e},expression:"user"}}),n("div",{staticClass:"k-login-buttons"},[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),n("k-button",{staticClass:"k-login-button",attrs:{icon:"check",type:"submit"}},[t._v("\n "+t._s(t.$t("login"))+" "),t.isLoading?[t._v("…")]:t._e()],2)],1)],1)},im=[],sm={data:function(){return{isLoading:!1,issue:"",user:{email:"",password:"",remember:!1}}},computed:{fields:function(){return{email:{autofocus:!0,label:this.$t("email"),type:"email",required:!0,link:!1},password:{label:this.$t("password"),type:"password",minLength:8,required:!0,autocomplete:"current-password",counter:!1}}}},methods:{login:function(){var t=this;this.issue=null,this.isLoading=!0,this.$store.dispatch("user/login",this.user).then(function(){t.$store.dispatch("system/load",!0).then(function(){t.$store.dispatch("notification/success",t.$t("welcome")),t.isLoading=!1})}).catch(function(){t.issue=t.$t("error.access.login"),t.isLoading=!1})}}},am=sm,om=Object(_["a"])(am,nm,im,!1,null,null,null),rm=om.exports,lm={components:{"k-login-form":window.panel.plugins.login||rm},data:function(){return{ready:!1,issue:null}},created:function(){var t=this;this.$store.dispatch("content/current",null),this.$store.dispatch("system/load").then(function(e){e.isReady||t.$router.push("/installation"),e.user&&e.user.id&&t.$router.push("/"),t.ready=!0,t.$store.dispatch("title",t.$t("login"))}).catch(function(e){t.issue=e})}},um=lm,cm=(n("24c1"),Object(_["a"])(um,tm,em,!1,null,null,null)),dm=cm.exports,pm=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.issue?n("k-error-view",[t._v("\n "+t._s(t.issue.message)+"\n")]):n("k-view",{staticClass:"k-page-view",attrs:{"data-locked":t.isLocked}},[n("k-header",{attrs:{tabs:t.tabs,tab:t.tab,editable:t.permissions.changeTitle&&!t.isLocked},on:{edit:function(e){return t.action("rename")}}},[t._v("\n "+t._s(t.page.title)+"\n "),n("k-button-group",{attrs:{slot:"left"},slot:"left"},[t.permissions.preview&&t.page.previewUrl?n("k-button",{attrs:{responsive:!0,link:t.page.previewUrl,target:"_blank",icon:"open"}},[t._v("\n "+t._s(t.$t("open"))+"\n ")]):t._e(),t.status?n("k-button",{class:["k-status-flag","k-status-flag-"+t.page.status],attrs:{disabled:!t.permissions.changeStatus||t.isLocked,icon:!t.permissions.changeStatus||t.isLocked?"protected":"circle",responsive:!0,tooltip:t.status.label},on:{click:function(e){return t.action("status")}}},[t._v("\n "+t._s(t.status.label)+"\n ")]):t._e(),n("k-dropdown",[n("k-button",{attrs:{responsive:!0,disabled:!0===t.isLocked,icon:"cog"},on:{click:function(e){return t.$refs.settings.toggle()}}},[t._v("\n "+t._s(t.$t("settings"))+"\n ")]),n("k-dropdown-content",{ref:"settings",attrs:{options:t.options},on:{action:t.action}})],1),n("k-languages-dropdown")],1),t.page.id?n("k-prev-next",{attrs:{slot:"right",prev:t.prev,next:t.next},slot:"right"}):t._e()],1),t.page.id?n("k-tabs",{key:t.tabsKey,ref:"tabs",attrs:{parent:t.$api.pages.url(t.page.id),blueprint:t.blueprint,tabs:t.tabs},on:{tab:t.onTab}}):t._e(),n("k-page-rename-dialog",{ref:"rename",on:{success:t.update}}),n("k-page-duplicate-dialog",{ref:"duplicate"}),n("k-page-url-dialog",{ref:"url"}),n("k-page-status-dialog",{ref:"status",on:{success:t.update}}),n("k-page-template-dialog",{ref:"template",on:{success:t.update}}),n("k-page-remove-dialog",{ref:"remove"})],1)},fm=[],hm={mixins:[Fh],props:{path:{type:String,required:!0}},data:function(){return{page:{title:"",id:null,prev:null,next:null,status:null},blueprint:null,preview:!0,permissions:{changeTitle:!1,changeStatus:!1},icon:"page",issue:null,tab:null,tabs:[],options:null}},computed:{language:function(){return this.$store.state.languages.current},next:function(){if(this.page.next)return{link:this.$api.pages.link(this.page.next.id),tooltip:this.page.next.title}},prev:function(){if(this.page.prev)return{link:this.$api.pages.link(this.page.prev.id),tooltip:this.page.prev.title}},status:function(){return null!==this.page.status?this.page.blueprint.status[this.page.status]:null},tabsKey:function(){return"page-"+this.page.id+"-tabs"}},watch:{language:function(){this.fetch()},path:function(){this.fetch()}},created:function(){this.$events.$on("page.changeSlug",this.update)},destroyed:function(){this.$events.$off("page.changeSlug",this.update)},methods:{action:function(t){switch(t){case"duplicate":this.$refs.duplicate.open(this.page.id);break;case"rename":this.$refs.rename.open(this.page.id);break;case"url":this.$refs.url.open(this.page.id);break;case"status":this.$refs.status.open(this.page.id);break;case"template":this.$refs.template.open(this.page.id);break;case"remove":this.$refs.remove.open(this.page.id);break;default:this.$store.dispatch("notification/error",this.$t("notification.notImplemented"));break}},fetch:function(){var t=this;this.$api.pages.get(this.path,{view:"panel"}).then(function(e){t.page=e,t.blueprint=e.blueprint.name,t.permissions=e.options,t.tabs=e.blueprint.tabs,t.options=function(e){t.$api.pages.options(t.page.id).then(function(t){e(t)})},t.$store.dispatch("breadcrumb",t.$api.pages.breadcrumb(e)),t.$store.dispatch("title",t.page.title),t.$store.dispatch("content/create",{id:"pages/"+t.page.id,api:t.$api.pages.link(t.page.id),content:t.page.content})}).catch(function(e){t.issue=e})},onTab:function(t){this.tab=t},update:function(){this.fetch(),this.$emit("model.update")}}},mm=hm,gm=(n("202d"),Object(_["a"])(mm,pm,fm,!1,null,null,null)),bm=gm.exports,vm=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-view",{staticClass:"k-settings-view"},[n("k-header",[t._v("\n "+t._s(t.$t("view.settings"))+"\n ")]),n("section",{staticClass:"k-system-info"},[n("header",[n("k-headline",[t._v("Kirby")])],1),n("ul",{staticClass:"k-system-info-box"},[n("li",[n("dl",[n("dt",[t._v(t._s(t.$t("license")))]),n("dd",[t.license?[t._v("\n "+t._s(t.license)+"\n ")]:n("p",[n("strong",{staticClass:"k-system-unregistered"},[t._v(t._s(t.$t("license.unregistered")))])])],2)])]),n("li",[n("dl",[n("dt",[t._v(t._s(t.$t("version")))]),n("dd",[t._v(t._s(t.$store.state.system.info.version))])])])])]),t.multilang?n("section",{staticClass:"k-languages"},[t.languages.length>0?[n("section",{staticClass:"k-languages-section"},[n("header",[n("k-headline",[t._v(t._s(t.$t("languages.default")))])],1),n("k-collection",{attrs:{items:t.defaultLanguage},on:{action:t.action}})],1),n("section",{staticClass:"k-languages-section"},[n("header",[n("k-headline",[t._v(t._s(t.$t("languages.secondary")))]),n("k-button",{attrs:{icon:"add"},on:{click:function(e){return t.$refs.create.open()}}},[t._v(t._s(t.$t("language.create")))])],1),t.translations.length?n("k-collection",{attrs:{items:t.translations},on:{action:t.action}}):n("k-empty",{attrs:{icon:"globe"},on:{click:function(e){return t.$refs.create.open()}}},[t._v(t._s(t.$t("languages.secondary.empty")))])],1)]:0===t.languages.length?[n("header",[n("k-headline",[t._v(t._s(t.$t("languages")))]),n("k-button",{attrs:{icon:"add"},on:{click:function(e){return t.$refs.create.open()}}},[t._v(t._s(t.$t("language.create")))])],1),n("k-empty",{attrs:{icon:"globe"},on:{click:function(e){return t.$refs.create.open()}}},[t._v(t._s(t.$t("languages.empty")))])]:t._e(),n("k-language-create-dialog",{ref:"create",on:{success:t.fetch}}),n("k-language-update-dialog",{ref:"update",on:{success:t.fetch}}),n("k-language-remove-dialog",{ref:"remove",on:{success:t.fetch}})],2):t._e()],1)},km=[],$m={data:function(){return{languages:[]}},computed:{defaultLanguage:function(){return this.languages.filter(function(t){return t.default})},multilang:function(){return this.$store.state.system.info.multilang},license:function(){return this.$store.state.system.info.license},translations:function(){return this.languages.filter(function(t){return!1===t.default})}},created:function(){this.$store.dispatch("content/current",null),this.$store.dispatch("title",this.$t("view.settings")),this.$store.dispatch("breadcrumb",[]),this.fetch()},methods:{fetch:function(){var t=this;!1!==this.multilang?this.$api.get("languages").then(function(e){t.languages=e.data.map(function(n){return{id:n.code,default:n.default,icon:{type:"globe",back:"black"},text:n.name,info:n.code,link:function(){t.$refs.update.open(n.code)},options:[{icon:"edit",text:t.$t("edit"),click:"update"},{icon:"trash",text:t.$t("delete"),disabled:n.default&&1!==e.data.length,click:"remove"}]}})}):this.languages=[]},action:function(t,e){switch(e){case"update":this.$refs.update.open(t.id);break;case"remove":this.$refs.remove.open(t.id);break}}}},_m=$m,ym=(n("9bd5"),Object(_["a"])(_m,vm,km,!1,null,null,null)),xm=ym.exports,wm=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.issue?n("k-error-view",[t._v("\n "+t._s(t.issue.message)+"\n")]):n("k-view",{key:"site-view",staticClass:"k-site-view",attrs:{"data-locked":t.isLocked}},[n("k-header",{attrs:{tabs:t.tabs,tab:t.tab,editable:t.permissions.changeTitle&&!t.isLocked},on:{edit:function(e){return t.action("rename")}}},[t._v("\n "+t._s(t.site.title)+"\n "),n("k-button-group",{attrs:{slot:"left"},slot:"left"},[n("k-button",{attrs:{responsive:!0,link:t.site.previewUrl,target:"_blank",icon:"open"}},[t._v("\n "+t._s(t.$t("open"))+"\n ")]),n("k-languages-dropdown")],1)],1),t.site.url?n("k-tabs",{ref:"tabs",attrs:{tabs:t.tabs,blueprint:t.site.blueprint.name,parent:"site"},on:{tab:function(e){t.tab=e}}}):t._e(),n("k-site-rename-dialog",{ref:"rename",on:{success:t.fetch}})],1)},Om=[],Cm={data:function(){return{site:{title:null,url:null},issue:null,tab:null,tabs:[],options:null,permissions:{changeTitle:!0}}},computed:{isLocked:function(){return null!==this.$store.state.content.status.lock},language:function(){return this.$store.state.languages.current}},watch:{language:function(){this.fetch()}},created:function(){this.fetch()},methods:{fetch:function(){var t=this;this.$api.site.get({view:"panel"}).then(function(e){t.site=e,t.tabs=e.blueprint.tabs,t.permissions=e.options,t.options=function(e){t.$api.site.options().then(function(t){e(t)})},t.$store.dispatch("breadcrumb",[]),t.$store.dispatch("title",null),t.$store.dispatch("content/create",{id:"site",api:"site",content:e.content})}).catch(function(e){t.issue=e})},action:function(t){switch(t){case"languages":this.$refs.languages.open();break;case"rename":this.$refs.rename.open();break;default:this.$store.dispatch("notification/error",this.$t("notification.notImplemented"));break}}}},Sm=Cm,Em=Object(_["a"])(Sm,wm,Om,!1,null,null,null),jm=Em.exports,Tm=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.issue?n("k-error-view",[t._v("\n "+t._s(t.issue.message)+"\n")]):n("k-view",{staticClass:"k-users-view"},[n("k-header",[t._v("\n "+t._s(t.$t("view.users"))+"\n "),n("k-button-group",{attrs:{slot:"left"},slot:"left"},[n("k-button",{attrs:{disabled:!1===t.$permissions.users.create,icon:"add"},on:{click:function(e){return t.$refs.create.open()}}},[t._v(t._s(t.$t("user.create")))])],1),n("k-button-group",{attrs:{slot:"right"},slot:"right"},[n("k-dropdown",[n("k-button",{attrs:{responsive:!0,icon:"funnel"},on:{click:function(e){return t.$refs.roles.toggle()}}},[t._v("\n "+t._s(t.$t("role"))+": "+t._s(t.role?t.role.text:t.$t("role.all"))+"\n ")]),n("k-dropdown-content",{ref:"roles",attrs:{align:"right"}},[n("k-dropdown-item",{attrs:{icon:"bolt"},on:{click:function(e){return t.filter(!1)}}},[t._v("\n "+t._s(t.$t("role.all"))+"\n ")]),n("hr"),t._l(t.roles,function(e){return n("k-dropdown-item",{key:e.value,attrs:{icon:"bolt"},on:{click:function(n){return t.filter(e)}}},[t._v("\n "+t._s(e.text)+"\n ")])})],2)],1)],1)],1),t.users.length>0?[n("k-collection",{attrs:{items:t.users,pagination:t.pagination},on:{paginate:t.paginate,action:t.action}})]:0===t.total?[n("k-empty",{attrs:{icon:"users"}},[t._v(t._s(t.$t("role.empty")))])]:t._e(),n("k-user-create-dialog",{ref:"create",on:{success:t.fetch}}),n("k-user-email-dialog",{ref:"email",on:{success:t.fetch}}),n("k-user-language-dialog",{ref:"language",on:{success:t.fetch}}),n("k-user-password-dialog",{ref:"password"}),n("k-user-remove-dialog",{ref:"remove",on:{success:t.fetch}}),n("k-user-rename-dialog",{ref:"rename",on:{success:t.fetch}}),n("k-user-role-dialog",{ref:"role",on:{success:t.fetch}})],2)},Im=[],Lm={data:function(){return{page:1,limit:20,total:null,users:[],roles:[],issue:null}},computed:{pagination:function(){return{page:this.page,limit:this.limit,total:this.total}},role:function(){var t=this,e=null;return this.$route.params.role&&this.roles.forEach(function(n){n.value===t.$route.params.role&&(e=n)}),e}},watch:{$route:function(){this.fetch()}},created:function(){var t=this;this.$store.dispatch("content/current",null),this.$api.roles.options().then(function(e){t.roles=e,t.fetch()})},methods:{fetch:function(){var t=this;this.$store.dispatch("title",this.$t("view.users"));var e={paginate:{page:this.page,limit:this.limit},sortBy:"username asc"};this.role&&(e.filterBy=[{field:"role",operator:"==",value:this.role.value}]),this.$api.users.list(e).then(function(e){t.users=e.data.map(function(e){var n={id:e.id,icon:{type:"user",back:"black"},text:e.name||e.email,info:e.role.title,link:"/users/"+e.id,options:function(n){t.$api.users.options(e.id,"list").then(function(t){return n(t)}).catch(function(e){t.$store.dispatch("notification/error",e)})},image:null};return e.avatar&&(n.image={url:e.avatar.url,cover:!0}),n}),t.role?t.$store.dispatch("breadcrumb",[{link:"/users/role/"+t.role.value,label:t.$t("role")+": "+t.role.text}]):t.$store.dispatch("breadcrumb",[]),t.total=e.pagination.total}).catch(function(e){t.issue=e})},paginate:function(t){this.page=t.page,this.limit=t.limit,this.fetch()},action:function(t,e){switch(e){case"edit":this.$router.push("/users/"+t.id);break;case"email":this.$refs.email.open(t.id);break;case"role":this.$refs.role.open(t.id);break;case"rename":this.$refs.rename.open(t.id);break;case"password":this.$refs.password.open(t.id);break;case"language":this.$refs.language.open(t.id);break;case"remove":this.$refs.remove.open(t.id);break}},filter:function(t){!1===t?this.$router.push("/users"):this.$router.push("/users/role/"+t.value),this.$refs.roles.close()}}},qm=Lm,Am=Object(_["a"])(qm,Tm,Im,!1,null,null,null),Nm=Am.exports,Bm=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.issue?n("k-error-view",[t._v("\n "+t._s(t.issue.message)+"\n")]):t.ready?n("div",{staticClass:"k-user-view",attrs:{"data-locked":t.isLocked}},[n("div",{staticClass:"k-user-profile"},[n("k-view",[t.avatar?[n("k-dropdown",[n("k-button",{staticClass:"k-user-view-image",attrs:{tooltip:t.$t("avatar"),disabled:t.isLocked},on:{click:function(e){return t.$refs.picture.toggle()}}},[t.avatar?n("k-image",{attrs:{cover:!0,src:t.avatar,ratio:"1/1"}}):t._e()],1),n("k-dropdown-content",{ref:"picture"},[n("k-dropdown-item",{attrs:{icon:"upload"},on:{click:function(e){return t.$refs.upload.open()}}},[t._v("\n "+t._s(t.$t("change"))+"\n ")]),n("k-dropdown-item",{attrs:{icon:"trash"},on:{click:function(e){return t.action("picture.delete")}}},[t._v("\n "+t._s(t.$t("delete"))+"\n ")])],1)],1)]:[n("k-button",{staticClass:"k-user-view-image",attrs:{tooltip:t.$t("avatar")},on:{click:function(e){return t.$refs.upload.open()}}},[n("k-icon",{attrs:{type:"user"}})],1)],n("k-button-group",[n("k-button",{attrs:{disabled:!t.permissions.changeEmail||t.isLocked,icon:"email"},on:{click:function(e){return t.action("email")}}},[t._v(t._s(t.$t("email"))+": "+t._s(t.user.email))]),n("k-button",{attrs:{disabled:!t.permissions.changeRole||t.isLocked,icon:"bolt"},on:{click:function(e){return t.action("role")}}},[t._v(t._s(t.$t("role"))+": "+t._s(t.user.role.title))]),n("k-button",{attrs:{disabled:!t.permissions.changeLanguage||t.isLocked,icon:"globe"},on:{click:function(e){return t.action("language")}}},[t._v(t._s(t.$t("language"))+": "+t._s(t.user.language))])],1)],2)],1),n("k-view",[n("k-header",{attrs:{editable:t.permissions.changeName&&!t.isLocked,tabs:t.tabs,tab:t.tab},on:{edit:function(e){return t.action("rename")}}},[t.user.name&&0!==t.user.name.length?[t._v(t._s(t.user.name))]:n("span",{staticClass:"k-user-name-placeholder"},[t._v(t._s(t.$t("name"))+" …")]),n("k-button-group",{attrs:{slot:"left"},slot:"left"},[n("k-dropdown",[n("k-button",{attrs:{disabled:t.isLocked,icon:"cog"},on:{click:function(e){return t.$refs.settings.toggle()}}},[t._v("\n "+t._s(t.$t("settings"))+"\n ")]),n("k-dropdown-content",{ref:"settings",attrs:{options:t.options},on:{action:t.action}})],1),n("k-languages-dropdown")],1),t.user.id&&"User"===t.$route.name?n("k-prev-next",{attrs:{slot:"right",prev:t.prev,next:t.next},slot:"right"}):t._e()],2),t.user&&t.tabs.length?n("k-tabs",{key:t.tabsKey,ref:"tabs",attrs:{parent:"users/"+t.user.id,blueprint:t.user.blueprint.name,tabs:t.tabs},on:{tab:function(e){t.tab=e}}}):t.ready?n("k-box",{attrs:{text:t.$t("user.blueprint",{role:t.user.role.name}),theme:"info"}}):t._e(),n("k-user-email-dialog",{ref:"email",on:{success:t.fetch}}),n("k-user-language-dialog",{ref:"language",on:{success:t.fetch}}),n("k-user-password-dialog",{ref:"password"}),n("k-user-remove-dialog",{ref:"remove"}),n("k-user-rename-dialog",{ref:"rename",on:{success:t.fetch}}),n("k-user-role-dialog",{ref:"role",on:{success:t.fetch}}),n("k-upload",{ref:"upload",attrs:{url:t.uploadApi,multiple:!1,accept:"image/*"},on:{success:t.uploadedAvatar}})],1)],1):t._e()},Pm=[],Dm={mixins:[Fh],props:{id:{type:String,required:!0}},data:function(){return{tab:null,tabs:[],ready:!1,user:{role:{name:null},name:null,language:null,prev:null,next:null},permissions:{changeEmail:!0,changeName:!0,changeLanguage:!0,changeRole:!0},issue:null,avatar:null,options:null}},computed:{language:function(){return this.$store.state.languages.current},next:function(){if(this.user.next)return{link:this.$api.users.link(this.user.next.id),tooltip:this.user.next.name}},prev:function(){if(this.user.prev)return{link:this.$api.users.link(this.user.prev.id),tooltip:this.user.prev.name}},tabsKey:function(){return"user-"+this.user.id+"-tabs"},uploadApi:function(){return A.api+"/users/"+this.user.id+"/avatar"}},watch:{language:function(){this.fetch()},id:function(){this.fetch()}},methods:{action:function(t){var e=this;switch(t){case"email":this.$refs.email.open(this.user.id);break;case"language":this.$refs.language.open(this.user.id);break;case"password":this.$refs.password.open(this.user.id);break;case"picture.delete":this.$api.users.deleteAvatar(this.id).then(function(){e.$store.dispatch("notification/success",":)"),e.avatar=null});break;case"remove":this.$refs.remove.open(this.user.id);break;case"rename":this.$refs.rename.open(this.user.id);break;case"role":this.$refs.role.open(this.user.id);break;default:this.$store.dispatch("notification/error","Not yet implemented")}},fetch:function(){var t=this;this.$api.users.get(this.id,{view:"panel"}).then(function(e){t.user=e,t.tabs=e.blueprint.tabs,t.ready=!0,t.permissions=e.options,t.options=function(e){t.$api.users.options(t.user.id).then(function(t){e(t)})},e.avatar?t.avatar=e.avatar.url:t.avatar=null,"User"===t.$route.name?t.$store.dispatch("breadcrumb",t.$api.users.breadcrumb(e)):t.$store.dispatch("breadcrumb",[]),t.$store.dispatch("title",t.user.name||t.user.email),t.$store.dispatch("content/create",{id:"users/"+e.id,api:t.$api.users.link(e.id),content:e.content})}).catch(function(e){t.issue=e})},uploadedAvatar:function(){this.$store.dispatch("notification/success",":)"),this.fetch()}}},Rm=Dm,Mm=(n("bd96"),Object(_["a"])(Rm,Bm,Pm,!1,null,null,null)),zm=Mm.exports;z["a"].component("k-dialog",Z),z["a"].component("k-error-dialog",it),z["a"].component("k-file-rename-dialog",mt),z["a"].component("k-file-remove-dialog",ut),z["a"].component("k-files-dialog",jt),z["a"].component("k-language-create-dialog",Nt),z["a"].component("k-language-remove-dialog",zt),z["a"].component("k-language-update-dialog",Wt),z["a"].component("k-page-create-dialog",te),z["a"].component("k-page-duplicate-dialog",oe),z["a"].component("k-page-rename-dialog",ve),z["a"].component("k-page-remove-dialog",pe),z["a"].component("k-page-status-dialog",we),z["a"].component("k-page-template-dialog",Te),z["a"].component("k-page-url-dialog",Be),z["a"].component("k-pages-dialog",Ue),z["a"].component("k-site-rename-dialog",Ve),z["a"].component("k-user-create-dialog",tn),z["a"].component("k-user-email-dialog",rn),z["a"].component("k-user-language-dialog",fn),z["a"].component("k-user-password-dialog",kn),z["a"].component("k-user-remove-dialog",On),z["a"].component("k-user-rename-dialog",In),z["a"].component("k-user-role-dialog",Pn),z["a"].component("k-users-dialog",Fn),z["a"].component("k-calendar",ei),z["a"].component("k-counter",ri),z["a"].component("k-autocomplete",Gn),z["a"].component("k-form",fi),z["a"].component("k-form-buttons",$i),z["a"].component("k-form-indicator",Ci),z["a"].component("k-field",Li),z["a"].component("k-fieldset",Di),z["a"].component("k-input",Hi),z["a"].component("k-upload",Xi),z["a"].component("k-checkbox-input",as),z["a"].component("k-checkboxes-input",ds),z["a"].component("k-date-input",ks),z["a"].component("k-datetime-input",Os),z["a"].component("k-email-input",Ns),z["a"].component("k-multiselect-input",zs),z["a"].component("k-number-input",Zs),z["a"].component("k-password-input",ea),z["a"].component("k-radio-input",ra),z["a"].component("k-range-input",fa),z["a"].component("k-select-input",ka),z["a"].component("k-tags-input",Oa),z["a"].component("k-tel-input",ja),z["a"].component("k-text-input",Is),z["a"].component("k-textarea-input",Na),z["a"].component("k-time-input",za),z["a"].component("k-toggle-input",Ya),z["a"].component("k-url-input",Za),z["a"].component("k-checkboxes-field",io),z["a"].component("k-date-field",uo),z["a"].component("k-email-field",go),z["a"].component("k-files-field",xo),z["a"].component("k-headline-field",jo),z["a"].component("k-info-field",No),z["a"].component("k-line-field",Mo),z["a"].component("k-multiselect-field",Vo),z["a"].component("k-number-field",Xo),z["a"].component("k-pages-field",sr),z["a"].component("k-password-field",cr),z["a"].component("k-radio-field",gr),z["a"].component("k-range-field",yr),z["a"].component("k-select-field",Er),z["a"].component("k-structure-field",Dr),z["a"].component("k-tags-field",Hr),z["a"].component("k-text-field",nl),z["a"].component("k-textarea-field",ll),z["a"].component("k-tel-field",Jr),z["a"].component("k-time-field",hl),z["a"].component("k-toggle-field",$l),z["a"].component("k-url-field",Cl),z["a"].component("k-users-field",Ll),z["a"].component("k-toolbar",Rl),z["a"].component("k-toolbar-email-dialog",Kl),z["a"].component("k-toolbar-link-dialog",Zl),z["a"].component("k-email-field-preview",hu),z["a"].component("k-files-field-preview",su),z["a"].component("k-pages-field-preview",$u),z["a"].component("k-toggle-field-preview",Cu),z["a"].component("k-url-field-preview",cu),z["a"].component("k-users-field-preview",Lu),z["a"].component("k-bar",Pu),z["a"].component("k-box",Fu),z["a"].component("k-card",Gu),z["a"].component("k-cards",ec),z["a"].component("k-collection",rc),z["a"].component("k-column",fc),z["a"].component("k-dropzone",kc),z["a"].component("k-empty",Oc),z["a"].component("k-file-preview",Ic),z["a"].component("k-grid",Pc),z["a"].component("k-header",Fc),z["a"].component("k-list",Gc),z["a"].component("k-list-item",ed),z["a"].component("k-tabs",rd),z["a"].component("k-view",fd),z["a"].component("k-draggable",_d),z["a"].component("k-error-boundary",Od),z["a"].component("k-headline",Id),z["a"].component("k-icon",Pd),z["a"].component("k-image",Fd),z["a"].component("k-progress",Gd),z["a"].component("k-sort-handle",tp),z["a"].component("k-text",op),z["a"].component("k-button",pp),z["a"].component("k-button-disabled",vp),z["a"].component("k-button-group",xp),z["a"].component("k-button-link",jp),z["a"].component("k-button-native",Bp),z["a"].component("k-dropdown",zp),z["a"].component("k-dropdown-content",Wp),z["a"].component("k-dropdown-item",tf),z["a"].component("k-languages-dropdown",ff),z["a"].component("k-link",rf),z["a"].component("k-pagination",kf),z["a"].component("k-prev-next",Of),z["a"].component("k-search",If),z["a"].component("k-tag",Pf),z["a"].component("k-topbar",Hf),z["a"].component("k-sections",Jf),z["a"].component("k-info-section",ih),z["a"].component("k-pages-section",ch),z["a"].component("k-files-section",gh),z["a"].component("k-fields-section",yh),z["a"].component("k-browser-view",jh),z["a"].component("k-custom-view",Nh),z["a"].component("k-error-view",Mh),z["a"].component("k-file-view",Yh),z["a"].component("k-installation-view",Qh),z["a"].component("k-login-view",dm),z["a"].component("k-page-view",bm),z["a"].component("k-settings-view",xm),z["a"].component("k-site-view",jm),z["a"].component("k-users-view",Nm),z["a"].component("k-user-view",zm);var Um={user:function(){return gg.get("auth")},login:function(t){var e={long:t.remember||!1,email:t.email,password:t.password};return gg.post("auth/login",e).then(function(t){return t.user})},logout:function(){return gg.post("auth/logout")}},Fm={get:function(t,e,n){return gg.get(this.url(t,e),n).then(function(t){return!0===xt()(t.content)&&(t.content={}),t})},update:function(t,e,n){return gg.patch(this.url(t,e),n)},rename:function(t,e,n){return gg.patch(this.url(t,e,"name"),{name:n})},url:function(t,e,n){var i=t+"/files/"+e;return n&&(i+="/"+n),i},link:function(t,e,n){return"/"+this.url(t,e,n)},delete:function(t,e){return gg.delete(this.url(t,e))},options:function(t,e,n){return gg.get(this.url(t,e),{select:"options"}).then(function(t){var e=t.options,i=[];return"list"===n&&i.push({icon:"open",text:z["a"].i18n.translate("open"),click:"download"}),i.push({icon:"title",text:z["a"].i18n.translate("rename"),click:"rename",disabled:!e.changeName}),i.push({icon:"upload",text:z["a"].i18n.translate("replace"),click:"replace",disabled:!e.replace}),i.push({icon:"trash",text:z["a"].i18n.translate("delete"),click:"remove",disabled:!e.delete}),i})},breadcrumb:function(t,e){var n=null,i=[];switch(e){case"UserFile":i.push({label:t.parent.username,link:gg.users.link(t.parent.id)}),n="users/"+t.parent.id;break;case"SiteFile":n="site";break;case"PageFile":i=t.parents.map(function(t){return{label:t.title,link:gg.pages.link(t.id)}}),n=gg.pages.url(t.parent.id);break}return i.push({label:t.filename,link:this.link(n,t.filename)}),i}},Hm={create:function(t,e){return null===t||"/"===t?gg.post("site/children",e):gg.post(this.url(t,"children"),e)},duplicate:function(t,e,n){return gg.post(this.url(t,"duplicate"),{slug:e,children:n.children||!1,files:n.files||!1})},url:function(t,e){var n=null===t?"pages":"pages/"+t.replace(/\//g,"+");return e&&(n+="/"+e),n},link:function(t){return"/"+this.url(t)},get:function(t,e){return gg.get(this.url(t),e).then(function(t){return!0===xt()(t.content)&&(t.content={}),t})},options:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"view";return gg.get(this.url(t),{select:"options"}).then(function(t){var n=t.options,i=[];return"list"===e&&(i.push({click:"preview",icon:"open",text:z["a"].i18n.translate("open"),disabled:!1===n.preview}),i.push("-")),i.push({click:"rename",icon:"title",text:z["a"].i18n.translate("rename"),disabled:!n.changeTitle}),i.push({click:"duplicate",icon:"copy",text:z["a"].i18n.translate("duplicate"),disabled:!n.duplicate}),i.push("-"),i.push({click:"url",icon:"url",text:z["a"].i18n.translate("page.changeSlug"),disabled:!n.changeSlug}),i.push({click:"status",icon:"preview",text:z["a"].i18n.translate("page.changeStatus"),disabled:!n.changeStatus}),i.push({click:"template",icon:"template",text:z["a"].i18n.translate("page.changeTemplate"),disabled:!n.changeTemplate}),i.push("-"),i.push({click:"remove",icon:"trash",text:z["a"].i18n.translate("delete"),disabled:!n.delete}),i})},preview:function(t){return this.get(t,{select:"previewUrl"}).then(function(t){return t.previewUrl})},update:function(t,e){return gg.patch(this.url(t),e)},children:function(t,e){return gg.post(this.url(t,"children/search"),e)},files:function(t,e){return gg.post(this.url(t,"files/search"),e)},delete:function(t,e){return gg.delete(this.url(t),e)},slug:function(t,e){return gg.patch(this.url(t,"slug"),{slug:e})},title:function(t,e){return gg.patch(this.url(t,"title"),{title:e})},template:function(t,e){return gg.patch(this.url(t,"template"),{template:e})},search:function(t,e){return t?gg.post("pages/"+t.replace("/","+")+"/children/search?select=id,title,hasChildren",e):gg.post("site/children/search?select=id,title,hasChildren",e)},status:function(t,e,n){return gg.patch(this.url(t,"status"),{status:e,position:n})},breadcrumb:function(t){var e=this,n=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],i=t.parents.map(function(t){return{label:t.title,link:e.link(t.id)}});return!0===n&&i.push({label:t.title,link:this.link(t.id)}),i}},Km=n("f499"),Vm=n.n(Km),Ym=n("2f62"),Wm=n("768b"),Gm=function(t){if(void 0!==t)return JSON.parse(Vm()(t))},Jm=function(t,e){localStorage.setItem("kirby$content$"+t,Vm()(e))},Zm={namespaced:!0,state:{current:null,models:{},status:{enabled:!0,lock:null,unlock:null}},getters:{exists:function(t){return function(e){return t.models.hasOwnProperty(e)}},hasChanges:function(t,e){return function(t){var n=e.model(t).changes;return kt()(n).length>0}},isCurrent:function(t){return function(e){return t.current===e}},id:function(t,e,n){return function(e){return e=e||t.current,n.languages.current?e+"/"+n.languages.current.code:e}},model:function(t,e){return function(n){return n=n||t.current,!0===e.exists(n)?t.models[n]:{api:null,originals:{},values:{},changes:{}}}},originals:function(t,e){return function(t){return Gm(e.model(t).originals)}},values:function(t,e){return function(t){return Object(I["a"])({},e.originals(t),e.changes(t))}},changes:function(t,e){return function(t){return Gm(e.model(t).changes)}}},mutations:{CREATE:function(t,e){var n=Object(Wm["a"])(e,2),i=n[0],s=n[1];if(!s)return!1;var a=t.models[i]?t.models[i].changes:s.changes;z["a"].set(t.models,i,{api:s.api,originals:s.originals,changes:a||{}})},CURRENT:function(t,e){t.current=e},LOCK:function(t,e){z["a"].set(t.status,"lock",e)},MOVE:function(t,e){var n=Object(Wm["a"])(e,2),i=n[0],s=n[1],a=Gm(t.models[i]);z["a"].delete(t.models,i),z["a"].set(t.models,s,a);var o=localStorage.getItem("kirby$content$"+i);localStorage.removeItem("kirby$content$"+i),localStorage.setItem("kirby$content$"+s,o)},REMOVE:function(t,e){z["a"].delete(t.models,e),localStorage.removeItem("kirby$content$"+e)},REVERT:function(t,e){t.models[e]&&(z["a"].set(t.models[e],"changes",{}),localStorage.removeItem("kirby$content$"+e))},STATUS:function(t,e){z["a"].set(t.status,"enabled",e)},UNLOCK:function(t,e){e&&z["a"].set(t.models[t.current],"changes",{}),z["a"].set(t.status,"unlock",e)},UPDATE:function(t,e){var n=Object(Wm["a"])(e,3),i=n[0],s=n[1],a=n[2];if(!t.models[i])return!1;a=Gm(a);var o=Vm()(a),r=Vm()(t.models[i].originals[s]);r===o?z["a"].delete(t.models[i].changes,s):z["a"].set(t.models[i].changes,s,a),Jm(i,{api:t.models[i].api,originals:t.models[i].originals,changes:t.models[i].changes})}},actions:{init:function(t){kt()(localStorage).filter(function(t){return t.startsWith("kirby$content$")}).map(function(t){return t.split("kirby$content$")[1]}).forEach(function(e){var n=localStorage.getItem("kirby$content$"+e);t.commit("CREATE",[e,JSON.parse(n)])}),kt()(localStorage).filter(function(t){return t.startsWith("kirby$form$")}).map(function(t){return t.split("kirby$form$")[1]}).forEach(function(e){var n=localStorage.getItem("kirby$form$"+e),i=null;try{i=JSON.parse(n)}catch(a){}if(!i||!i.api)return localStorage.removeItem("kirby$form$"+e),!1;var s={api:i.api,originals:i.originals,changes:i.values};t.commit("CREATE",[e,s]),Jm(e,s),localStorage.removeItem("kirby$form$"+e)})},create:function(t,e){e.id=t.getters.id(e.id),(e.id.startsWith("pages/")||e.id.startsWith("site"))&&delete e.content.title;var n={api:e.api,originals:Gm(e.content),changes:{}};gg.get(e.api+"/unlock").then(function(n){!0===n.supported&&!0===n.unlocked&&t.commit("UNLOCK",t.state.models[e.id].changes)}).catch(function(){}),t.commit("CREATE",[e.id,n]),t.dispatch("current",e.id)},current:function(t,e){t.commit("CURRENT",e)},disable:function(t){t.commit("STATUS",!1)},enable:function(t){t.commit("STATUS",!0)},lock:function(t,e){t.commit("LOCK",e)},move:function(t,e){var n=Object(Wm["a"])(e,2),i=n[0],s=n[1];i=t.getters.id(i),s=t.getters.id(s),t.commit("MOVE",[i,s])},remove:function(t,e){t.commit("REMOVE",e),t.getters.isCurrent(e)&&t.commit("CURRENT",null)},revert:function(t,e){e=e||t.state.current,t.commit("REVERT",e)},save:function(t,e){if(e=e||t.state.current,t.getters.isCurrent(e)&&!1===t.state.status.enabled)return!1;t.dispatch("disable");var n=t.getters.model(e),i=Object(I["a"])({},n.originals,n.changes);return gg.patch(n.api,i).then(function(){t.commit("CREATE",[e,Object(I["a"])({},n,{originals:i})]),t.dispatch("revert",e),t.dispatch("enable")}).catch(function(e){throw t.dispatch("enable"),e})},unlock:function(t,e){t.commit("UNLOCK",e)},update:function(t,e){var n=Object(Wm["a"])(e,3),i=n[0],s=n[1],a=n[2];a=a||t.state.current,t.commit("UPDATE",[a,i,s])}}},Xm={namespaced:!0,state:{instance:null,clock:0,step:5,beats:[]},mutations:{ADD:function(t,e){t.beats.push(e)},CLEAR:function(t){clearInterval(t.instance),t.clock=0},CLOCK:function(t){t.clock+=t.step},INITIALIZE:function(t,e){t.instance=e},REMOVE:function(t,e){var n=t.beats.map(function(t){return t.handler}).indexOf(e);-1!==n&&z["a"].delete(t.beats,n)}},actions:{add:function(t,e){e={handler:e[0]||e,interval:e[1]||t.state.step},e.handler(),t.commit("ADD",e),1===t.state.beats.length&&t.dispatch("run")},clear:function(t){t.commit("CLEAR")},remove:function(t,e){t.commit("REMOVE",e),t.state.beats.length<1&&t.commit("CLEAR")},run:function(t){t.commit("CLEAR"),t.commit("INITIALIZE",setInterval(function(){t.commit("CLOCK"),t.state.beats.forEach(function(e){t.state.clock%e.interval===0&&e.handler()})},1e3*t.state.step))}}},Qm={namespaced:!0,state:{all:[],current:null,default:null},mutations:{SET_ALL:function(t,e){t.all=e.map(function(t){return{code:t.code,default:t.default,direction:t.direction,locale:t.locale,name:t.name,rules:t.rules,url:t.url}})},SET_CURRENT:function(t,e){t.current=e,e&&e.code&&localStorage.setItem("kirby$language",e.code)},SET_DEFAULT:function(t,e){t.default=e}},actions:{current:function(t,e){t.commit("SET_CURRENT",e)},install:function(t,e){var n=e.filter(function(t){return t.default})[0];t.commit("SET_ALL",e),t.commit("SET_DEFAULT",n);var i=localStorage.getItem("kirby$language");if(i){var s=e.filter(function(t){return t.code===i})[0];if(s)return void t.dispatch("current",s)}t.dispatch("current",n||e[0]||null)},load:function(t){return gg.get("languages").then(function(e){t.dispatch("install",e.data)})}}},tg={timer:null,namespaced:!0,state:{type:null,message:null,details:null,timeout:null},mutations:{SET:function(t,e){t.type=e.type,t.message=e.message,t.details=e.details,t.timeout=e.timeout},UNSET:function(t){t.type=null,t.message=null,t.details=null,t.timeout=null}},actions:{close:function(t){clearTimeout(this.timer),t.commit("UNSET")},open:function(t,e){t.dispatch("close"),t.commit("SET",e),e.timeout&&(this.timer=setTimeout(function(){t.dispatch("close")},e.timeout))},success:function(t,e){"string"===typeof e&&(e={message:e}),t.dispatch("open",Object(I["a"])({type:"success",timeout:4e3},e))},error:function(t,e){"string"===typeof e&&(e={message:e}),t.dispatch("open",Object(I["a"])({type:"error"},e))}}},eg={namespaced:!0,state:{info:{title:null}},mutations:{SET_INFO:function(t,e){t.info=e},SET_LICENSE:function(t,e){t.info.license=e},SET_TITLE:function(t,e){t.info.title=e}},actions:{title:function(t,e){t.commit("SET_TITLE",e)},register:function(t,e){t.commit("SET_LICENSE",e)},load:function(t,e){return!e&&t.state.info.isReady&&t.rootState.user.current?new Je.a(function(e){e(t.state.info)}):gg.system.info({view:"panel"}).then(function(e){return t.commit("SET_INFO",Object(I["a"])({isReady:e.isInstalled&&e.isOk},e)),e.languages&&t.dispatch("languages/install",e.languages,{root:!0}),t.dispatch("translation/install",e.translation,{root:!0}),t.dispatch("translation/activate",e.translation.id,{root:!0}),e.user&&t.dispatch("user/current",e.user,{root:!0}),t.state.info}).catch(function(e){t.commit("SET_INFO",{isBroken:!0,error:e.message})})}}},ng={namespaced:!0,state:{current:null,installed:[]},mutations:{SET_CURRENT:function(t,e){t.current=e},INSTALL:function(t,e){t.installed[e.id]=e}},actions:{load:function(t,e){return gg.translations.get(e)},install:function(t,e){t.commit("INSTALL",e),z["a"].i18n.add(e.id,e.data)},activate:function(t,e){var n=t.state.installed[e];n?(z["a"].i18n.set(e),t.commit("SET_CURRENT",e),document.dir=n.direction,document.documentElement.lang=e):t.dispatch("load",e).then(function(n){t.dispatch("install",n),t.dispatch("activate",e)})}}},ig=n("8c4f"),sg=function(t,e,n){ug.dispatch("system/load").then(function(){var e=ug.state.user.current;if(!e)return ug.dispatch("user/visit",t.path),ug.dispatch("user/logout"),!1;var i=e.permissions.access;return!1===i.panel?(window.location.href=A.site,!1):!1===i[t.meta.view]?(ug.dispatch("notification/error",{message:z["a"].i18n.translate("error.access.view")}),n("/")):void n()})},ag=[{path:"/",name:"Home",redirect:"/site"},{path:"/browser",name:"Browser",component:z["a"].component("k-browser-view"),meta:{outside:!0}},{path:"/login",component:z["a"].component("k-login-view"),meta:{outside:!0}},{path:"/logout",beforeEnter:function(){kt()(localStorage).forEach(function(t){t.startsWith("kirby$content$")&&localStorage.removeItem(t)}),ug.dispatch("user/logout")},meta:{outside:!0}},{path:"/installation",component:z["a"].component("k-installation-view"),meta:{outside:!0}},{path:"/site",name:"Site",meta:{view:"site"},component:z["a"].component("k-site-view"),beforeEnter:sg},{path:"/site/files/:filename",name:"SiteFile",meta:{view:"site"},component:z["a"].component("k-file-view"),beforeEnter:sg,props:function(t){return{path:"site",filename:t.params.filename}}},{path:"/pages/:path/files/:filename",name:"PageFile",meta:{view:"site"},component:z["a"].component("k-file-view"),beforeEnter:sg,props:function(t){return{path:"pages/"+t.params.path,filename:t.params.filename}}},{path:"/users/:path/files/:filename",name:"UserFile",meta:{view:"users"},component:z["a"].component("k-file-view"),beforeEnter:sg,props:function(t){return{path:"users/"+t.params.path,filename:t.params.filename}}},{path:"/pages/:path",name:"Page",meta:{view:"site"},component:z["a"].component("k-page-view"),beforeEnter:sg,props:function(t){return{path:t.params.path}}},{path:"/settings",name:"Settings",meta:{view:"settings"},component:z["a"].component("k-settings-view"),beforeEnter:sg},{path:"/users/role/:role",name:"UsersByRole",meta:{view:"users"},component:z["a"].component("k-users-view"),beforeEnter:sg,props:function(t){return{role:t.params.role}}},{path:"/users",name:"Users",meta:{view:"users"},beforeEnter:sg,component:z["a"].component("k-users-view")},{path:"/users/:id",name:"User",meta:{view:"users"},component:z["a"].component("k-user-view"),beforeEnter:sg,props:function(t){return{id:t.params.id}}},{path:"/account",name:"Account",meta:{view:"account"},component:z["a"].component("k-user-view"),beforeEnter:sg,props:function(){return{id:ug.state.user.current?ug.state.user.current.id:null}}},{path:"/plugins/:id",name:"Plugin",meta:{view:"plugin"},props:function(t){return{plugin:t.params.id}},beforeEnter:sg,component:z["a"].component("k-custom-view")},{path:"*",name:"NotFound",beforeEnter:function(t,e,n){n("/")}}];z["a"].use(ig["a"]);var og=new ig["a"]({mode:"history",routes:ag,url:"/"===A.url?"":A.url});og.beforeEach(function(t,e,n){"Browser"!==t.name&&!1===Oh.all()&&n("/browser"),t.meta.outside||ug.dispatch("user/visit",t.path),ug.dispatch("view",t.meta.view),ug.dispatch("content/lock",null),ug.dispatch("content/unlock",null),ug.dispatch("heartbeat/clear"),n()});var rg=og,lg={namespaced:!0,state:{current:null,path:null},mutations:{SET_CURRENT:function(t,e){t.current=e,e&&e.permissions?(z["a"].prototype.$user=e,z["a"].prototype.$permissions=e.permissions):(z["a"].prototype.$user=null,z["a"].prototype.$permissions=null)},SET_PATH:function(t,e){t.path=e}},actions:{current:function(t,e){t.commit("SET_CURRENT",e)},email:function(t,e){t.commit("SET_CURRENT",Object(I["a"])({},t.state.current,{email:e}))},language:function(t,e){t.dispatch("translation/activate",e,{root:!0}),t.commit("SET_CURRENT",Object(I["a"])({},t.state.current,{language:e}))},load:function(t){return gg.auth.user().then(function(e){return t.commit("SET_CURRENT",e),e})},login:function(t,e){return gg.auth.login(e).then(function(e){return t.commit("SET_CURRENT",e),t.dispatch("translation/activate",e.language,{root:!0}),rg.push(t.state.path||"/"),e})},logout:function(t,e){t.commit("SET_CURRENT",null),e?window.location.href=(window.panel.url||"")+"/login":gg.auth.logout().then(function(){rg.push("/login")}).catch(function(){rg.push("/login")})},name:function(t,e){t.commit("SET_CURRENT",Object(I["a"])({},t.state.current,{name:e}))},visit:function(t,e){t.commit("SET_PATH",e)}}};z["a"].use(Ym["a"]);var ug=new Ym["a"].Store({strict:!1,state:{breadcrumb:[],dialog:null,drag:null,isLoading:!1,search:!1,title:null,view:null},mutations:{SET_BREADCRUMB:function(t,e){t.breadcrumb=e},SET_DIALOG:function(t,e){t.dialog=e},SET_DRAG:function(t,e){t.drag=e},SET_SEARCH:function(t,e){!0===e&&(e={}),t.search=e},SET_TITLE:function(t,e){t.title=e},SET_VIEW:function(t,e){t.view=e},START_LOADING:function(t){t.isLoading=!0},STOP_LOADING:function(t){t.isLoading=!1}},actions:{breadcrumb:function(t,e){t.commit("SET_BREADCRUMB",e)},dialog:function(t,e){t.commit("SET_DIALOG",e)},drag:function(t,e){t.commit("SET_DRAG",e)},isLoading:function(t,e){t.commit(!0===e?"START_LOADING":"STOP_LOADING")},search:function(t,e){t.commit("SET_SEARCH",e)},title:function(t,e){t.commit("SET_TITLE",e),document.title=e||"",t.state.system.info.title&&(document.title+=null!==e?" | "+t.state.system.info.title:t.state.system.info.title)},view:function(t,e){t.commit("SET_VIEW",e)}},modules:{content:Zm,heartbeat:Xm,languages:Qm,notification:tg,system:eg,translation:ng,user:lg}}),cg={running:0,request:function(t,e){var n=this,i=arguments.length>2&&void 0!==arguments[2]&&arguments[2];e=Wi()(e||{},{credentials:"same-origin",cache:"no-store",headers:Object(I["a"])({"x-requested-with":"xmlhttprequest","content-type":"application/json"},e.headers)}),ug.state.languages.current&&(e.headers["x-language"]=ug.state.languages.current.code),e.headers["x-csrf"]=window.panel.csrf;var s=t+"/"+Vm()(e);return gg.config.onStart(s,i),this.running++,fetch(gg.config.endpoint+"/"+t,e).then(function(t){return t.text()}).then(function(t){try{return JSON.parse(t)}catch(e){throw new Error("The JSON response from the API could not be parsed. Please check your API connection.")}}).then(function(t){if(t.status&&"error"===t.status)throw t;var e=t;return t.data&&t.type&&"model"===t.type&&(e=t.data),n.running--,gg.config.onComplete(s),gg.config.onSuccess(t),e}).catch(function(t){throw n.running--,gg.config.onComplete(s),gg.config.onError(t),t})},get:function(t,e,n){var i=arguments.length>3&&void 0!==arguments[3]&&arguments[3];return e&&(t+="?"+kt()(e).map(function(t){var n=e[t];return void 0!==n&&null!==n?t+"="+n:null}).filter(function(t){return null!==t}).join("&")),this.request(t,Wi()(n||{},{method:"GET"}),i)},post:function(t,e,n){var i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"POST",s=arguments.length>4&&void 0!==arguments[4]&&arguments[4];return this.request(t,Wi()(n||{},{method:i,body:Vm()(e)}),s)},patch:function(t,e,n){var i=arguments.length>3&&void 0!==arguments[3]&&arguments[3];return this.post(t,e,n,"PATCH",i)},delete:function(t,e,n){var i=arguments.length>3&&void 0!==arguments[3]&&arguments[3];return this.post(t,e,n,"DELETE",i)}},dg={list:function(t){return gg.get("roles",t)},get:function(t){return gg.get("roles/"+t)},options:function(t){return this.list(t).then(function(t){return t.data.map(function(t){return{info:t.description||"(".concat(z["a"].i18n.translate("role.description.placeholder"),")"),text:t.title,value:t.name}})})}},pg={info:function(t){return gg.get("system",t)},install:function(t){return gg.post("system/install",t).then(function(t){return t.user})},register:function(t){return gg.post("system/register",t)}},fg={get:function(t){return gg.get("site",t)},update:function(t){return gg.post("site",t)},title:function(t){return gg.patch("site/title",{title:t})},options:function(){return gg.get("site",{select:"options"}).then(function(t){var e=t.options,n=[];return n.push({click:"rename",icon:"title",text:z["a"].i18n.translate("rename"),disabled:!e.changeTitle}),n})},children:function(t){return gg.post("site/children/search",t)},blueprint:function(){return gg.get("site/blueprint")},blueprints:function(){return gg.get("site/blueprints")}},hg={list:function(){return gg.get("translations")},get:function(t){return gg.get("translations/"+t)},options:function(){var t=[];return this.list().then(function(e){return t=e.data.map(function(t){return{value:t.id,text:t.name}}),t})}},mg={create:function(t){return gg.post(this.url(),t)},list:function(t){return gg.post(this.url(null,"search"),t)},get:function(t,e){return gg.get(this.url(t),e)},update:function(t,e){return gg.patch(this.url(t),e)},delete:function(t){return gg.delete(this.url(t))},changeEmail:function(t,e){return gg.patch(this.url(t,"email"),{email:e})},changeLanguage:function(t,e){return gg.patch(this.url(t,"language"),{language:e})},changeName:function(t,e){return gg.patch(this.url(t,"name"),{name:e})},changePassword:function(t,e){return gg.patch(this.url(t,"password"),{password:e})},changeRole:function(t,e){return gg.patch(this.url(t,"role"),{role:e})},deleteAvatar:function(t){return gg.delete(this.url(t,"avatar"))},blueprint:function(t){return gg.get(this.url(t,"blueprint"))},breadcrumb:function(t){return[{link:"/users/"+t.id,label:t.username}]},options:function(t){return gg.get(this.url(t),{select:"options"}).then(function(t){var e=t.options,n=[];return n.push({click:"rename",icon:"title",text:z["a"].i18n.translate("user.changeName"),disabled:!e.changeName}),n.push({click:"email",icon:"email",text:z["a"].i18n.translate("user.changeEmail"),disabled:!e.changeEmail}),n.push({click:"role",icon:"bolt",text:z["a"].i18n.translate("user.changeRole"),disabled:!e.changeRole}),n.push({click:"password",icon:"key",text:z["a"].i18n.translate("user.changePassword"),disabled:!e.changePassword}),n.push({click:"language",icon:"globe",text:z["a"].i18n.translate("user.changeLanguage"),disabled:!e.changeLanguage}),n.push({click:"remove",icon:"trash",text:z["a"].i18n.translate("user.delete"),disabled:!e.delete}),n})},url:function(t,e){var n=t?"users/"+t:"users";return e&&(n+="/"+e),n},link:function(t,e){return"/"+this.url(t,e)}},gg=Object(I["a"])({config:{onStart:function(){},onComplete:function(){},onSuccess:function(){},onError:function(t){throw window.console.log(t.message),t}},auth:Um,files:Fm,pages:Hm,roles:dg,system:pg,site:fg,translations:hg,users:mg},cg);gg.config.endpoint=A.api,gg.requests=[],gg.config.onStart=function(t,e){!1===e&&ug.dispatch("isLoading",!0),gg.requests.push(t)},gg.config.onComplete=function(t){gg.requests=gg.requests.filter(function(e){return e!==t}),0===gg.requests.length&&ug.dispatch("isLoading",!1)},gg.config.onError=function(t){A.debug&&window.console.error(t),403!==t.code||"Unauthenticated"!==t.message&&"access.panel"!==t.key||ug.dispatch("user/logout",!0)};var bg=setInterval(gg.auth.user,3e5);gg.config.onSuccess=function(){clearInterval(bg),bg=setInterval(gg.auth.user,3e5)},z["a"].prototype.$api=gg,z["a"].config.errorHandler=function(t){A.debug&&window.console.error(t),ug.dispatch("notification/error",{message:t.message||"An error occurred. Please reload the panel"})},window.panel=window.panel||{},window.panel.error=function(t,e){A.debug&&window.console.error(t+": "+e),ug.dispatch("error",t+". See the console for more information.")},RegExp.escape=function(t){return t.replace(new RegExp("[-/\\\\^$*+?.()[\\]{}]","gu"),"\\$&")};var vg=function(t,e){t=String(t);var n="";e=(e||2)-t.length;while(n.length0&&void 0!==arguments[0]?arguments[0]:"3/2",e=String(t).split("/");if(2!==e.length)return"100%";var n=Number(e[0]),i=Number(e[1]),s=100;return 0!==n&&0!==i&&(s=100/n*i),s+"%"},$g=function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[],n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"",i="-";return n="a-z0-9"+n,t=t.trim().toLowerCase(),e.forEach(function(e){e&&kt()(e).forEach(function(n){var i="/"!==n.substr(0,1),s=n.substring(1,n.length-1),a=i?n:s;t=t.replace(new RegExp(RegExp.escape(a),"g"),e[n])})}),t=t.replace("/[^\t\n\r -~]/",""),t=t.replace(new RegExp("[^"+n+"]","ig"),i),t=t.replace(new RegExp("["+RegExp.escape(i)+"]{2,}","g"),i),t=t.replace("/",i),t=t.replace(new RegExp("^[^"+n+"]+","g"),""),t=t.replace(new RegExp("[^"+n+"]+$","g"),""),t},_g=function(t){t=t||{};var e=t.desc?-1:1,n=-e,i=/^0/,s=/\s+/g,a=/^\s+|\s+$/g,o=/[^\x00-\x80]/,r=/^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 d(""+t).replace(a,"")}:function(t){return(""+t).replace(a,"")};function d(t){return t.toLocaleLowerCase?t.toLocaleLowerCase():t.toLowerCase()}function p(t){return t.replace(l,"\0$1\0").replace(/\0$/,"").replace(/^\0/,"").split("\0")}function f(t,e){return(!t.match(i)||1===e)&&Ys()(t)||t.replace(s," ").replace(a,"")||0}return function(t,i){var s=c(t),a=c(i);if(!s&&!a)return 0;if(!s&&a)return n;if(s&&!a)return e;var l=p(s),d=p(a),h=ms()(s.match(r),16)||1!==l.length&&Date.parse(s),m=ms()(a.match(r),16)||h&&a.match(u)&&Date.parse(a)||null;if(m){if(hm)return e}for(var g=l.length,b=d.length,v=0,k=Math.max(g,b);v0)return e;if(y<0)return n;if(v===k-1)return 0}else{if($<_)return n;if($>_)return e}}return 0}},yg={ucfirst:function(t){var e=String(t);return e.charAt(0).toUpperCase()+e.substr(1)},lcfirst:function(t){var e=String(t);return e.charAt(0).toLowerCase()+e.substr(1)}},xg=function(t,e){var n={url:"/",field:"file",method:"POST",accept:"text",attributes:{},complete:function(){},error:function(){},success:function(){},progress:function(){}},i=Wi()(n,e),s=new FormData;s.append(i.field,t,t.name),i.attributes&&kt()(i.attributes).forEach(function(t){s.append(t,i.attributes[t])});var a=new XMLHttpRequest,o=function(e){if(e.lengthComputable&&i.progress){var n=Math.max(0,Math.min(100,e.loaded/e.total*100));i.progress(a,t,Math.ceil(n))}};a.addEventListener("loadstart",o),a.addEventListener("progress",o),a.addEventListener("load",function(e){var n=null;try{n=JSON.parse(e.target.response)}catch(s){n={status:"error",message:"The file could not be uploaded"}}n.status&&"error"===n.status?i.error(a,t,n):(i.success(a,t,n),i.progress(a,t,100))}),a.addEventListener("error",function(e){var n=JSON.parse(e.target.response);i.error(a,t,n),i.progress(a,t,100)}),a.open("POST",i.url,!0),i.headers&&kt()(i.headers).forEach(function(t){var e=i.headers[t];a.setRequestHeader(t,e)}),a.send(s)},wg=function(t){return!!t.dataTransfer&&(!!t.dataTransfer.types&&(!0===t.dataTransfer.types.includes("Files")&&!1===t.dataTransfer.types.includes("text/plain")))};z["a"].prototype.$helper={clone:Gm,isUploadEvent:wg,debounce:wt,pad:vg,ratio:kg,slug:$g,sort:_g,string:yg,upload:xg};var Og=n("f2f3");z["a"].use(Og["a"].plugin,ug);var Cg=n("19e9"),Sg=n.n(Cg),Eg=n("5a0c"),jg=n.n(Eg),Tg=n("f906"),Ig=n.n(Tg);jg.a.extend(Ig.a),z["a"].prototype.$library={autosize:Sg.a,dayjs:jg.a};var Lg=n("2d1f"),qg=n.n(Lg),Ag={};for(var Ng in z["a"].options.components)Ag[Ng]=z["a"].options.components[Ng];var Bg=function(t,e){e.template||e.render||e.extends?(e.extends&&"string"===typeof e.extends&&(e.extends=Ag[e.extends],e.template&&(e.render=null)),e.mixins&&(e.mixins=e.mixins.map(function(t){return"string"===typeof t?Ag[t]:t})),Ag[t]&&window.console.warn('Plugin is replacing "'.concat(t,'"')),z["a"].component(t,e)):ug.dispatch("notification/error",'Neither template or render method provided nor extending a component when loading plugin component "'.concat(t,'". The component has not been registered.'))};qg()(window.panel.plugins.components).forEach(function(t){var e=Object(Wm["a"])(t,2),n=e[0],i=e[1];Bg(n,i)}),qg()(window.panel.plugins.fields).forEach(function(t){var e=Object(Wm["a"])(t,2),n=e[0],i=e[1];Bg(n,i)}),qg()(window.panel.plugins.sections).forEach(function(t){var e=Object(Wm["a"])(t,2),n=e[0],i=e[1];Bg(n,Object(I["a"])({},i,{mixins:[Qf].concat(i.mixins||[])}))}),qg()(window.panel.plugins.views).forEach(function(t){var e=Object(Wm["a"])(t,2),n=e[0],i=e[1];if(!i.component)return ug.dispatch("notification/error",'No view component provided when loading view "'.concat(n,'". The view has not been registered.')),void delete window.panel.plugins.views[n];i.link="/plugins/"+n,void 0===i.icon&&(i.icon="page"),void 0===i.menu&&(i.menu=!0),window.panel.plugins.views[n]={link:i.link,icon:i.icon,menu:i.menu},z["a"].component("k-"+n+"-plugin-view",i.component)}),window.panel.plugins.use.forEach(function(t){z["a"].use(t)}),z["a"].config.productionTip=!1,z["a"].config.devtools=!0,z["a"].use(M),z["a"].use(R),z["a"].use(F.a),new z["a"]({router:rg,store:ug,created:function(){var t=this;window.panel.app=this,window.panel.plugins.created.forEach(function(e){e(t)}),this.$store.dispatch("content/init")},render:function(t){return t(D)}}).$mount("#app")},5714:function(t,e,n){},"580a":function(t,e,n){"use strict";var i=n("61ab"),s=n.n(i);s.a},"589a":function(t,e,n){},"58e5":function(t,e,n){},"5ab5":function(t,e,n){},"5aee":function(t,e,n){"use strict";var i=n("04b2"),s=n.n(i);s.a},"5b23":function(t,e,n){"use strict";var i=n("9798"),s=n.n(i);s.a},"5c0b":function(t,e,n){"use strict";var i=n("5e27"),s=n.n(i);s.a},"5d33":function(t,e,n){"use strict";var i=n("2246"),s=n.n(i);s.a},"5e27":function(t,e,n){},"5f12":function(t,e,n){},6018:function(t,e,n){"use strict";var i=n("e30b"),s=n.n(i);s.a},"61ab":function(t,e,n){},"64e4":function(t,e,n){"use strict";var i=n("1340"),s=n.n(i);s.a},"64e6":function(t,e,n){},"65a9":function(t,e,n){},"696b5":function(t,e,n){"use strict";var i=n("0cdc"),s=n.n(i);s.a},"6a18":function(t,e,n){"use strict";var i=n("de8a"),s=n.n(i);s.a},"6ab3":function(t,e,n){"use strict";var i=n("784e"),s=n.n(i);s.a},"6ab9":function(t,e,n){},"6b7f":function(t,e,n){},"6bcd":function(t,e,n){"use strict";var i=n("9e0a"),s=n.n(i);s.a},"6e56":function(t,e,n){},"6f7b":function(t,e,n){"use strict";var i=n("5ab5"),s=n.n(i);s.a},7075:function(t,e,n){},"718c":function(t,e,n){"use strict";var i=n("773d"),s=n.n(i);s.a},7568:function(t,e,n){"use strict";var i=n("4150"),s=n.n(i);s.a},"75cd":function(t,e,n){},7737:function(t,e,n){"use strict";var i=n("ca19"),s=n.n(i);s.a},"773d":function(t,e,n){},"778b":function(t,e,n){},7797:function(t,e,n){},"784e":function(t,e,n){},"7a7d":function(t,e,n){"use strict";var i=n("65a9"),s=n.n(i);s.a},"7d2d":function(t,e,n){},"7d5d":function(t,e,n){"use strict";var i=n("6ab9"),s=n.n(i);s.a},"7dc7":function(t,e,n){"use strict";var i=n("eb17"),s=n.n(i);s.a},"7e0c":function(t,e,n){},"7e85":function(t,e,n){"use strict";var i=n("d1c5"),s=n.n(i);s.a},"7f6e":function(t,e,n){"use strict";var i=n("4364"),s=n.n(i);s.a},"862b":function(t,e,n){"use strict";var i=n("589a"),s=n.n(i);s.a},"893d":function(t,e,n){"use strict";var i=n("abb3"),s=n.n(i);s.a},"8ae6":function(t,e,n){},"8c28":function(t,e,n){"use strict";var i=n("3d5b"),s=n.n(i);s.a},"8e4d":function(t,e,n){},"910b":function(t,e,n){},"957b":function(t,e,n){},9749:function(t,e,n){},"977f":function(t,e,n){"use strict";var i=n("b7f5"),s=n.n(i);s.a},9798:function(t,e,n){},9799:function(t,e,n){"use strict";var i=n("4fe0"),s=n.n(i);s.a},9811:function(t,e,n){},"98a1":function(t,e,n){"use strict";var i=n("f0cb"),s=n.n(i);s.a},"9bd5":function(t,e,n){"use strict";var i=n("64e6"),s=n.n(i);s.a},"9df7":function(t,e,n){},"9e0a":function(t,e,n){},"9e26":function(t,e,n){"use strict";var i=n("a440"),s=n.n(i);s.a},a134:function(t,e,n){"use strict";var i=n("4390"),s=n.n(i);s.a},a440:function(t,e,n){},a567:function(t,e,n){"use strict";var i=n("c0b5"),s=n.n(i);s.a},a5f3:function(t,e,n){"use strict";var i=n("43f4"),s=n.n(i);s.a},a66d:function(t,e,n){"use strict";var i=n("2eb5"),s=n.n(i);s.a},a79d:function(t,e,n){},abb3:function(t,e,n){},ac27:function(t,e,n){"use strict";var i=n("3c9d"),s=n.n(i);s.a},b0d6:function(t,e,n){"use strict";var i=n("d31d"),s=n.n(i);s.a},b37e:function(t,e,n){},b3c3:function(t,e,n){},b5d2:function(t,e,n){"use strict";var i=n("ed7b"),s=n.n(i);s.a},b746:function(t,e,n){"use strict";var i=n("7e0c"),s=n.n(i);s.a},b7f5:function(t,e,n){},ba8f:function(t,e,n){"use strict";var i=n("9749"),s=n.n(i);s.a},bb41:function(t,e,n){"use strict";var i=n("ceb4"),s=n.n(i);s.a},bd96:function(t,e,n){"use strict";var i=n("d6a4"),s=n.n(i);s.a},bf53:function(t,e,n){"use strict";var i=n("3c80"),s=n.n(i);s.a},c0b5:function(t,e,n){},c119:function(t,e,n){"use strict";var i=n("4b49"),s=n.n(i);s.a},c7c8:function(t,e,n){"use strict";var i=n("1be2"),s=n.n(i);s.a},c857:function(t,e,n){"use strict";var i=n("7d2d"),s=n.n(i);s.a},c9cb:function(t,e,n){"use strict";var i=n("b37e"),s=n.n(i);s.a},ca19:function(t,e,n){},ca3a:function(t,e,n){},cb8f:function(t,e,n){"use strict";var i=n("8e4d"),s=n.n(i);s.a},cc79:function(t,e,n){"use strict";var i=n("a79d"),s=n.n(i);s.a},cca8:function(t,e,n){"use strict";var i=n("18b7"),s=n.n(i);s.a},ceb4:function(t,e,n){},d0c1:function(t,e,n){"use strict";var i=n("9df7"),s=n.n(i);s.a},d0e7:function(t,e,n){},d1c5:function(t,e,n){},d221:function(t,e,n){"use strict";var i=n("6b7f"),s=n.n(i);s.a},d31d:function(t,e,n){},d6a4:function(t,e,n){},d6c1:function(t,e,n){},d6fc:function(t,e,n){"use strict";var i=n("08ec"),s=n.n(i);s.a},d9c4:function(t,e,n){},daa8:function(t,e,n){"use strict";var i=n("e60b"),s=n.n(i);s.a},db92:function(t,e,n){},ddfd:function(t,e,n){"use strict";var i=n("4dc8"),s=n.n(i);s.a},de8a:function(t,e,n){},df0d:function(t,e,n){"use strict";var i=n("3ab9"),s=n.n(i);s.a},e30b:function(t,e,n){},e60b:function(t,e,n){},e697:function(t,e,n){},eb17:function(t,e,n){},ec72:function(t,e,n){},ed7b:function(t,e,n){},ee15:function(t,e,n){"use strict";var i=n("fd81"),s=n.n(i);s.a},f0cb:function(t,e,n){},f56d:function(t,e,n){"use strict";var i=n("75cd"),s=n.n(i);s.a},f5e3:function(t,e,n){},f8a7:function(t,e,n){"use strict";var i=n("db92"),s=n.n(i);s.a},f95f:function(t,e,n){"use strict";var i=n("5f12"),s=n.n(i);s.a},fa6a:function(t,e,n){"use strict";var i=n("778b"),s=n.n(i);s.a},fb1a:function(t,e,n){},fc0f:function(t,e,n){"use strict";var i=n("424a"),s=n.n(i);s.a},fd81:function(t,e,n){},ff6d:function(t,e,n){},fffc:function(t,e,n){}}); \ No newline at end of file diff --git a/kirby/panel/dist/js/plugins.js b/kirby/panel/dist/js/plugins.js new file mode 100755 index 0000000..d386ebd --- /dev/null +++ b/kirby/panel/dist/js/plugins.js @@ -0,0 +1,67 @@ + +window.panel = window.panel || {}; +window.panel.plugins = { + components: {}, + created: [], + fields: {}, + icons: {}, + sections: {}, + routes: [], + use: [], + views: {}, +}; + +window.panel.plugin = function (plugin, parts) { + // Components + resolve(parts, "components", function (name, options) { + window.panel.plugins["components"][name] = options; + }); + + // Fields + resolve(parts, "fields", function (name, options) { + window.panel.plugins["fields"][`k-${name}-field`] = options; + }); + + // Icons + resolve(parts, "icons", function (name, options) { + window.panel.plugins["icons"][name] = options; + }); + + // Sections + resolve(parts, "sections", function (name, options) { + window.panel.plugins["sections"][`k-${name}-section`] = options; + }); + + // Vue.use + resolve(parts, "use", function (name, options) { + window.panel.plugins["use"].push(options); + }); + + // created callback + if (parts["created"]) { + window.panel.plugins["created"].push(parts["created"]); + } + + // Views + resolve(parts, "views", function (name, options) { + window.panel.plugins["views"][name] = options; + }); + + // Login + if (parts.login) { + window.panel.plugins.login = parts.login; + } + +}; + +function resolve(object, type, callback) { + if (object[type]) { + + if (Object.entries) { + Object.entries(object[type]).forEach(function ([name, options]) { + callback(name, options); + }); + } + + } +} diff --git a/kirby/panel/dist/js/vendor.js b/kirby/panel/dist/js/vendor.js new file mode 100755 index 0000000..87294a1 --- /dev/null +++ b/kirby/panel/dist/js/vendor.js @@ -0,0 +1,41 @@ +(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-vendors"],{"0029":function(t,e){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},"0185":function(t,e,n){var r=n("e5fa");t.exports=function(t){return Object(r(t))}},"01f9":function(t,e,n){"use strict";var r=n("2d00"),o=n("5ca1"),i=n("2aba"),a=n("32e9"),c=n("84f2"),s=n("41a0"),u=n("7f20"),f=n("38fd"),l=n("2b4c")("iterator"),p=!([].keys&&"next"in[].keys()),d="@@iterator",h="keys",v="values",y=function(){return this};t.exports=function(t,e,n,g,m,b,_){s(n,e,g);var w,x,O,S=function(t){if(!p&&t in k)return k[t];switch(t){case h:return function(){return new n(this,t)};case v:return function(){return new n(this,t)}}return function(){return new n(this,t)}},$=e+" Iterator",A=m==v,C=!1,k=t.prototype,E=k[l]||k[d]||m&&k[m],M=E||S(m),j=m?A?S("entries"):M:void 0,T="Array"==e&&k.entries||E;if(T&&(O=f(T.call(new t)),O!==Object.prototype&&O.next&&(u(O,$,!0),r||"function"==typeof O[l]||a(O,l,y))),A&&E&&E.name!==v&&(C=!0,M=function(){return E.call(this)}),r&&!_||!p&&!C&&k[l]||a(k,l,M),c[e]=M,c[$]=y,m)if(w={values:A?M:S(v),keys:b?M:S(h),entries:j},_)for(x in w)x in k||i(k,x,w[x]);else o(o.P+o.F*(p||C),e,w);return w}},"0234":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=Object.assign||function(t){for(var e=1;e=u?t?"":void 0:(i=c.charCodeAt(s),i<55296||i>56319||s+1===u||(a=c.charCodeAt(s+1))<56320||a>57343?t?c.charAt(s):i:t?c.slice(s,s+2):a-56320+(i-55296<<10)+65536)}}},"0390":function(t,e,n){"use strict";var r=n("02f4")(!0);t.exports=function(t,e,n){return e+(n?r(t,e).length:1)}},"03ca":function(t,e,n){"use strict";var r=n("f2fe");function o(t){var e,n;this.promise=new t(function(t,r){if(void 0!==e||void 0!==n)throw TypeError("Bad Promise constructor");e=t,n=r}),this.resolve=r(e),this.reject=r(n)}t.exports.f=function(t){return new o(t)}},"04cf":function(t,e,n){var r=n("4a89"),o=Math.min;t.exports=function(t){return t>0?o(r(t),9007199254740991):0}},"08c1":function(t,e,n){"use strict";var r,o,i,a,c=n("e6a1"),s=n("b808"),u=n("a9f2"),f=n("a274"),l=n("569f"),p=n("ab4c"),d=n("9184"),h=n("8115"),v=n("88b8"),y=n("1aa7"),g=n("1ad4").set,m=n("a24c")(),b=n("cc20"),_=n("73c3"),w=n("4b9e"),x=n("1c08"),O="Promise",S=s.TypeError,$=s.process,A=$&&$.versions,C=A&&A.v8||"",k=s[O],E="process"==f($),M=function(){},j=o=b.f,T=!!function(){try{var t=k.resolve(1),e=(t.constructor={})[n("b67f")("species")]=function(t){t(M,M)};return(E||"function"==typeof PromiseRejectionEvent)&&t.then(M)instanceof e&&0!==C.indexOf("6.6")&&-1===w.indexOf("Chrome/66")}catch(r){}}(),P=function(t){var e;return!(!p(t)||"function"!=typeof(e=t.then))&&e},D=function(t,e){if(!t._n){t._n=!0;var n=t._c;m(function(){var r=t._v,o=1==t._s,i=0,a=function(e){var n,i,a,c=o?e.ok:e.fail,s=e.resolve,u=e.reject,f=e.domain;try{c?(o||(2==t._h&&N(t),t._h=1),!0===c?n=r:(f&&f.enter(),n=c(r),f&&(f.exit(),a=!0)),n===e.promise?u(S("Promise-chain cycle")):(i=P(n))?i.call(n,s,u):s(n)):u(r)}catch(l){f&&!a&&f.exit(),u(l)}};while(n.length>i)a(n[i++]);t._c=[],t._n=!1,e&&!t._h&&L(t)})}},L=function(t){g.call(s,function(){var e,n,r,o=t._v,i=I(t);if(i&&(e=_(function(){E?$.emit("unhandledRejection",o,t):(n=s.onunhandledrejection)?n({promise:t,reason:o}):(r=s.console)&&r.error&&r.error("Unhandled promise rejection",o)}),t._h=E||I(t)?2:1),t._a=void 0,i&&e.e)throw e.v})},I=function(t){return 1!==t._h&&0===(t._a||t._c).length},N=function(t){g.call(s,function(){var e;E?$.emit("rejectionHandled",t):(e=s.onrejectionhandled)&&e({promise:t,reason:t._v})})},R=function(t){var e=this;e._d||(e._d=!0,e=e._w||e,e._v=t,e._s=2,e._a||(e._a=e._c.slice()),D(e,!0))},F=function(t){var e,n=this;if(!n._d){n._d=!0,n=n._w||n;try{if(n===t)throw S("Promise can't be resolved itself");(e=P(t))?m(function(){var r={_w:n,_d:!1};try{e.call(t,u(F,r,1),u(R,r,1))}catch(o){R.call(r,o)}}):(n._v=t,n._s=1,D(n,!1))}catch(r){R.call({_w:n,_d:!1},r)}}};T||(k=function(t){h(this,k,O,"_h"),d(t),r.call(this);try{t(u(F,this,1),u(R,this,1))}catch(e){R.call(this,e)}},r=function(t){this._c=[],this._a=void 0,this._s=0,this._d=!1,this._v=void 0,this._h=0,this._n=!1},r.prototype=n("9faf")(k.prototype,{then:function(t,e){var n=j(y(this,k));return n.ok="function"!=typeof t||t,n.fail="function"==typeof e&&e,n.domain=E?$.domain:void 0,this._c.push(n),this._a&&this._a.push(n),this._s&&D(this,!1),n.promise},catch:function(t){return this.then(void 0,t)}}),i=function(){var t=new r;this.promise=t,this.resolve=u(F,t,1),this.reject=u(R,t,1)},b.f=j=function(t){return t===k||t===a?new i(t):o(t)}),l(l.G+l.W+l.F*!T,{Promise:k}),n("aab6")(k,O),n("0ec0")(O),a=n("ca38")[O],l(l.S+l.F*!T,O,{reject:function(t){var e=j(this),n=e.reject;return n(t),e.promise}}),l(l.S+l.F*(c||!T),O,{resolve:function(t){return x(c&&this===a?k:this,t)}}),l(l.S+l.F*!(T&&n("2299")(function(t){k.all(t)["catch"](M)})),O,{all:function(t){var e=this,n=j(e),r=n.resolve,o=n.reject,i=_(function(){var n=[],i=0,a=1;v(t,!1,function(t){var c=i++,s=!1;n.push(void 0),a++,e.resolve(t).then(function(t){s||(s=!0,n[c]=t,--a||r(n))},o)}),--a||r(n)});return i.e&&o(i.v),n.promise},race:function(t){var e=this,n=j(e),r=n.reject,o=_(function(){v(t,!1,function(t){e.resolve(t).then(n.resolve,r)})});return o.e&&r(o.v),n.promise}})},"0965":function(t,e,n){n("384f"),t.exports=n("a7d3").parseFloat},"0a0a":function(t,e,n){var r=n("da3c"),o=n("a7d3"),i=n("b457"),a=n("fda1"),c=n("3adc").f;t.exports=function(t){var e=o.Symbol||(o.Symbol=i?{}:r.Symbol||{});"_"==t.charAt(0)||t in e||c(e,t,{value:a.f(t)})}},"0a49":function(t,e,n){var r=n("9b43"),o=n("626a"),i=n("4bf8"),a=n("9def"),c=n("cd1c");t.exports=function(t,e){var n=1==t,s=2==t,u=3==t,f=4==t,l=6==t,p=5==t||l,d=e||c;return function(e,c,h){for(var v,y,g=i(e),m=o(g),b=r(c,h,3),_=a(m.length),w=0,x=n?d(e,_):s?d(e,0):void 0;_>w;w++)if((p||w in m)&&(v=m[w],y=b(v,w,g),t))if(n)x[w]=y;else if(y)switch(t){case 3:return!0;case 5:return v;case 6:return w;case 2:x.push(v)}else if(f)return!1;return l?-1:u||f?f:x}}},"0a91":function(t,e,n){n("b42c"),n("93c4"),t.exports=n("b77f")},"0bfb":function(t,e,n){"use strict";var r=n("cb7c");t.exports=function(){var t=r(this),e="";return t.global&&(e+="g"),t.ignoreCase&&(e+="i"),t.multiline&&(e+="m"),t.unicode&&(e+="u"),t.sticky&&(e+="y"),e}},"0d58":function(t,e,n){var r=n("ce10"),o=n("e11e");t.exports=Object.keys||function(t){return r(t,o)}},"0ec0":function(t,e,n){"use strict";var r=n("b808"),o=n("e4e1"),i=n("45e2"),a=n("b67f")("species");t.exports=function(t){var e=r[t];i&&e&&!e[a]&&o.f(e,a,{configurable:!0,get:function(){return this}})}},"0f4a":function(t,e){var n={}.hasOwnProperty;t.exports=function(t,e){return n.call(t,e)}},"0f89":function(t,e,n){var r=n("6f8a");t.exports=function(t){if(!r(t))throw TypeError(t+" is not an object!");return t}},"103a":function(t,e,n){var r=n("da3c").document;t.exports=r&&r.documentElement},1169:function(t,e,n){var r=n("2d95");t.exports=Array.isArray||function(t){return"Array"==r(t)}},"11e9":function(t,e,n){var r=n("52a7"),o=n("4630"),i=n("6821"),a=n("6a99"),c=n("69a8"),s=n("c69a"),u=Object.getOwnPropertyDescriptor;e.f=n("9e1e")?u:function(t,e){if(t=i(t),e=a(e,!0),s)try{return u(t,e)}catch(n){}if(c(t,e))return o(!r.f.call(t,e),t[e])}},"11ff":function(t,e,n){var r=n("da3c").parseFloat,o=n("633a").trim;t.exports=1/r(n("702a")+"-0")!==-1/0?function(t){var e=o(String(t),3),n=r(e);return 0===n&&"-"==e.charAt(0)?-0:n}:r},"12fd":function(t,e,n){var r=n("6f8a"),o=n("da3c").document,i=r(o)&&r(o.createElement);t.exports=function(t){return i?o.createElement(t):{}}},"12fd9":function(t,e){},1495:function(t,e,n){var r=n("86cc"),o=n("cb7c"),i=n("0d58");t.exports=n("9e1e")?Object.defineProperties:function(t,e){o(t);var n,a=i(e),c=a.length,s=0;while(c>s)r.f(t,n=a[s++],e[n]);return t}},"14c6":function(t,e,n){"use strict";var r=n("3bb1"),o=n("b5cb"),i=n("a638"),a=n("58b9");t.exports=n("fa2d")(Array,"Array",function(t,e){this._t=a(t),this._i=0,this._k=e},function(){var t=this._t,e=this._k,n=this._i++;return!t||n>=t.length?(this._t=void 0,o(1)):o(0,"keys"==e?n:"values"==e?t[n]:[n,t[n]])},"values"),i.Arguments=i.Array,r("keys"),r("values"),r("entries")},"16e7":function(t,e,n){var r=n("d13f"),o=n("7704");r(r.G+r.F*(parseInt!=o),{parseInt:o})},1938:function(t,e,n){var r=n("d13f");r(r.S,"Array",{isArray:n("b5aa")})},"196c":function(t,e){t.exports=function(t,e,n){var r=void 0===n;switch(e.length){case 0:return r?t():t.call(n);case 1:return r?t(e[0]):t.call(n,e[0]);case 2:return r?t(e[0],e[1]):t.call(n,e[0],e[1]);case 3:return r?t(e[0],e[1],e[2]):t.call(n,e[0],e[1],e[2]);case 4:return r?t(e[0],e[1],e[2],e[3]):t.call(n,e[0],e[1],e[2],e[3])}return t.apply(n,e)}},1980:function(t,e,n){(function(e,r){t.exports=r(n("53fe"))})("undefined"!==typeof self&&self,function(t){return function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"===typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t["default"]}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s="fb15")}({"02f4":function(t,e,n){var r=n("4588"),o=n("be13");t.exports=function(t){return function(e,n){var i,a,c=String(o(e)),s=r(n),u=c.length;return s<0||s>=u?t?"":void 0:(i=c.charCodeAt(s),i<55296||i>56319||s+1===u||(a=c.charCodeAt(s+1))<56320||a>57343?t?c.charAt(s):i:t?c.slice(s,s+2):a-56320+(i-55296<<10)+65536)}}},"0390":function(t,e,n){"use strict";var r=n("02f4")(!0);t.exports=function(t,e,n){return e+(n?r(t,e).length:1)}},"07e3":function(t,e){var n={}.hasOwnProperty;t.exports=function(t,e){return n.call(t,e)}},"0bfb":function(t,e,n){"use strict";var r=n("cb7c");t.exports=function(){var t=r(this),e="";return t.global&&(e+="g"),t.ignoreCase&&(e+="i"),t.multiline&&(e+="m"),t.unicode&&(e+="u"),t.sticky&&(e+="y"),e}},"0fc9":function(t,e,n){var r=n("3a38"),o=Math.max,i=Math.min;t.exports=function(t,e){return t=r(t),t<0?o(t+e,0):i(t,e)}},1654:function(t,e,n){"use strict";var r=n("71c1")(!0);n("30f1")(String,"String",function(t){this._t=String(t),this._i=0},function(){var t,e=this._t,n=this._i;return n>=e.length?{value:void 0,done:!0}:(t=r(e,n),this._i+=t.length,{value:t,done:!1})})},1691:function(t,e){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},"1af6":function(t,e,n){var r=n("63b6");r(r.S,"Array",{isArray:n("9003")})},"1bc3":function(t,e,n){var r=n("f772");t.exports=function(t,e){if(!r(t))return t;var n,o;if(e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;if("function"==typeof(n=t.valueOf)&&!r(o=n.call(t)))return o;if(!e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;throw TypeError("Can't convert object to primitive value")}},"1ec9":function(t,e,n){var r=n("f772"),o=n("e53d").document,i=r(o)&&r(o.createElement);t.exports=function(t){return i?o.createElement(t):{}}},"20fd":function(t,e,n){"use strict";var r=n("d9f6"),o=n("aebd");t.exports=function(t,e,n){e in t?r.f(t,e,o(0,n)):t[e]=n}},"214f":function(t,e,n){"use strict";n("b0c5");var r=n("2aba"),o=n("32e9"),i=n("79e5"),a=n("be13"),c=n("2b4c"),s=n("520a"),u=c("species"),f=!i(function(){var t=/./;return t.exec=function(){var t=[];return t.groups={a:"7"},t},"7"!=="".replace(t,"$
")}),l=function(){var t=/(?:)/,e=t.exec;t.exec=function(){return e.apply(this,arguments)};var n="ab".split(t);return 2===n.length&&"a"===n[0]&&"b"===n[1]}();t.exports=function(t,e,n){var p=c(t),d=!i(function(){var e={};return e[p]=function(){return 7},7!=""[t](e)}),h=d?!i(function(){var e=!1,n=/a/;return n.exec=function(){return e=!0,null},"split"===t&&(n.constructor={},n.constructor[u]=function(){return n}),n[p](""),!e}):void 0;if(!d||!h||"replace"===t&&!f||"split"===t&&!l){var v=/./[p],y=n(a,p,""[t],function(t,e,n,r,o){return e.exec===s?d&&!o?{done:!0,value:v.call(e,n,r)}:{done:!0,value:t.call(n,e,r)}:{done:!1}}),g=y[0],m=y[1];r(String.prototype,t,g),o(RegExp.prototype,p,2==e?function(t,e){return m.call(t,this,e)}:function(t){return m.call(t,this)})}}},"230e":function(t,e,n){var r=n("d3f4"),o=n("7726").document,i=r(o)&&r(o.createElement);t.exports=function(t){return i?o.createElement(t):{}}},"23c6":function(t,e,n){var r=n("2d95"),o=n("2b4c")("toStringTag"),i="Arguments"==r(function(){return arguments}()),a=function(t,e){try{return t[e]}catch(n){}};t.exports=function(t){var e,n,c;return void 0===t?"Undefined":null===t?"Null":"string"==typeof(n=a(e=Object(t),o))?n:i?r(e):"Object"==(c=r(e))&&"function"==typeof e.callee?"Arguments":c}},"241e":function(t,e,n){var r=n("25eb");t.exports=function(t){return Object(r(t))}},"25eb":function(t,e){t.exports=function(t){if(void 0==t)throw TypeError("Can't call method on "+t);return t}},"294c":function(t,e){t.exports=function(t){try{return!!t()}catch(e){return!0}}},"2aba":function(t,e,n){var r=n("7726"),o=n("32e9"),i=n("69a8"),a=n("ca5a")("src"),c=n("fa5b"),s="toString",u=(""+c).split(s);n("8378").inspectSource=function(t){return c.call(t)},(t.exports=function(t,e,n,c){var s="function"==typeof n;s&&(i(n,"name")||o(n,"name",e)),t[e]!==n&&(s&&(i(n,a)||o(n,a,t[e]?""+t[e]:u.join(String(e)))),t===r?t[e]=n:c?t[e]?t[e]=n:o(t,e,n):(delete t[e],o(t,e,n)))})(Function.prototype,s,function(){return"function"==typeof this&&this[a]||c.call(this)})},"2b4c":function(t,e,n){var r=n("5537")("wks"),o=n("ca5a"),i=n("7726").Symbol,a="function"==typeof i,c=t.exports=function(t){return r[t]||(r[t]=a&&i[t]||(a?i:o)("Symbol."+t))};c.store=r},"2d00":function(t,e){t.exports=!1},"2d95":function(t,e){var n={}.toString;t.exports=function(t){return n.call(t).slice(8,-1)}},"2fdb":function(t,e,n){"use strict";var r=n("5ca1"),o=n("d2c8"),i="includes";r(r.P+r.F*n("5147")(i),"String",{includes:function(t){return!!~o(this,t,i).indexOf(t,arguments.length>1?arguments[1]:void 0)}})},"30f1":function(t,e,n){"use strict";var r=n("b8e3"),o=n("63b6"),i=n("9138"),a=n("35e8"),c=n("481b"),s=n("8f60"),u=n("45f2"),f=n("53e2"),l=n("5168")("iterator"),p=!([].keys&&"next"in[].keys()),d="@@iterator",h="keys",v="values",y=function(){return this};t.exports=function(t,e,n,g,m,b,_){s(n,e,g);var w,x,O,S=function(t){if(!p&&t in k)return k[t];switch(t){case h:return function(){return new n(this,t)};case v:return function(){return new n(this,t)}}return function(){return new n(this,t)}},$=e+" Iterator",A=m==v,C=!1,k=t.prototype,E=k[l]||k[d]||m&&k[m],M=E||S(m),j=m?A?S("entries"):M:void 0,T="Array"==e&&k.entries||E;if(T&&(O=f(T.call(new t)),O!==Object.prototype&&O.next&&(u(O,$,!0),r||"function"==typeof O[l]||a(O,l,y))),A&&E&&E.name!==v&&(C=!0,M=function(){return E.call(this)}),r&&!_||!p&&!C&&k[l]||a(k,l,M),c[e]=M,c[$]=y,m)if(w={values:A?M:S(v),keys:b?M:S(h),entries:j},_)for(x in w)x in k||i(k,x,w[x]);else o(o.P+o.F*(p||C),e,w);return w}},"32a6":function(t,e,n){var r=n("241e"),o=n("c3a1");n("ce7e")("keys",function(){return function(t){return o(r(t))}})},"32e9":function(t,e,n){var r=n("86cc"),o=n("4630");t.exports=n("9e1e")?function(t,e,n){return r.f(t,e,o(1,n))}:function(t,e,n){return t[e]=n,t}},"32fc":function(t,e,n){var r=n("e53d").document;t.exports=r&&r.documentElement},"335c":function(t,e,n){var r=n("6b4c");t.exports=Object("z").propertyIsEnumerable(0)?Object:function(t){return"String"==r(t)?t.split(""):Object(t)}},"355d":function(t,e){e.f={}.propertyIsEnumerable},"35e8":function(t,e,n){var r=n("d9f6"),o=n("aebd");t.exports=n("8e60")?function(t,e,n){return r.f(t,e,o(1,n))}:function(t,e,n){return t[e]=n,t}},"36c3":function(t,e,n){var r=n("335c"),o=n("25eb");t.exports=function(t){return r(o(t))}},3702:function(t,e,n){var r=n("481b"),o=n("5168")("iterator"),i=Array.prototype;t.exports=function(t){return void 0!==t&&(r.Array===t||i[o]===t)}},"3a38":function(t,e){var n=Math.ceil,r=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?r:n)(t)}},"40c3":function(t,e,n){var r=n("6b4c"),o=n("5168")("toStringTag"),i="Arguments"==r(function(){return arguments}()),a=function(t,e){try{return t[e]}catch(n){}};t.exports=function(t){var e,n,c;return void 0===t?"Undefined":null===t?"Null":"string"==typeof(n=a(e=Object(t),o))?n:i?r(e):"Object"==(c=r(e))&&"function"==typeof e.callee?"Arguments":c}},4588:function(t,e){var n=Math.ceil,r=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?r:n)(t)}},"45f2":function(t,e,n){var r=n("d9f6").f,o=n("07e3"),i=n("5168")("toStringTag");t.exports=function(t,e,n){t&&!o(t=n?t:t.prototype,i)&&r(t,i,{configurable:!0,value:e})}},4630:function(t,e){t.exports=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}}},"469f":function(t,e,n){n("6c1c"),n("1654"),t.exports=n("7d7b")},"481b":function(t,e){t.exports={}},"4aa6":function(t,e,n){t.exports=n("dc62")},"4bf8":function(t,e,n){var r=n("be13");t.exports=function(t){return Object(r(t))}},"4ee1":function(t,e,n){var r=n("5168")("iterator"),o=!1;try{var i=[7][r]();i["return"]=function(){o=!0},Array.from(i,function(){throw 2})}catch(a){}t.exports=function(t,e){if(!e&&!o)return!1;var n=!1;try{var i=[7],c=i[r]();c.next=function(){return{done:n=!0}},i[r]=function(){return c},t(i)}catch(a){}return n}},"50ed":function(t,e){t.exports=function(t,e){return{value:e,done:!!t}}},5147:function(t,e,n){var r=n("2b4c")("match");t.exports=function(t){var e=/./;try{"/./"[t](e)}catch(n){try{return e[r]=!1,!"/./"[t](e)}catch(o){}}return!0}},5168:function(t,e,n){var r=n("dbdb")("wks"),o=n("62a0"),i=n("e53d").Symbol,a="function"==typeof i,c=t.exports=function(t){return r[t]||(r[t]=a&&i[t]||(a?i:o)("Symbol."+t))};c.store=r},5176:function(t,e,n){t.exports=n("51b6")},"51b6":function(t,e,n){n("a3c3"),t.exports=n("584a").Object.assign},"520a":function(t,e,n){"use strict";var r=n("0bfb"),o=RegExp.prototype.exec,i=String.prototype.replace,a=o,c="lastIndex",s=function(){var t=/a/,e=/b*/g;return o.call(t,"a"),o.call(e,"a"),0!==t[c]||0!==e[c]}(),u=void 0!==/()??/.exec("")[1],f=s||u;f&&(a=function(t){var e,n,a,f,l=this;return u&&(n=new RegExp("^"+l.source+"$(?!\\s)",r.call(l))),s&&(e=l[c]),a=o.call(l,t),s&&a&&(l[c]=l.global?a.index+a[0].length:e),u&&a&&a.length>1&&i.call(a[0],n,function(){for(f=1;f1?arguments[1]:void 0,y=void 0!==v,g=0,m=f(p);if(y&&(v=r(v,h>2?arguments[2]:void 0,2)),void 0==m||d==Array&&c(m))for(e=s(p.length),n=new d(e);e>g;g++)u(n,g,y?v(p[g],g):p[g]);else for(l=m.call(p),n=new d;!(o=l.next()).done;g++)u(n,g,y?a(l,v,[o.value,g],!0):o.value);return n.length=g,n}})},"54a1":function(t,e,n){n("6c1c"),n("1654"),t.exports=n("95d5")},5537:function(t,e,n){var r=n("8378"),o=n("7726"),i="__core-js_shared__",a=o[i]||(o[i]={});(t.exports=function(t,e){return a[t]||(a[t]=void 0!==e?e:{})})("versions",[]).push({version:r.version,mode:n("2d00")?"pure":"global",copyright:"© 2019 Denis Pushkarev (zloirock.ru)"})},5559:function(t,e,n){var r=n("dbdb")("keys"),o=n("62a0");t.exports=function(t){return r[t]||(r[t]=o(t))}},"584a":function(t,e){var n=t.exports={version:"2.6.5"};"number"==typeof __e&&(__e=n)},"5b4e":function(t,e,n){var r=n("36c3"),o=n("b447"),i=n("0fc9");t.exports=function(t){return function(e,n,a){var c,s=r(e),u=o(s.length),f=i(a,u);if(t&&n!=n){while(u>f)if(c=s[f++],c!=c)return!0}else for(;u>f;f++)if((t||f in s)&&s[f]===n)return t||f||0;return!t&&-1}}},"5ca1":function(t,e,n){var r=n("7726"),o=n("8378"),i=n("32e9"),a=n("2aba"),c=n("9b43"),s="prototype",u=function(t,e,n){var f,l,p,d,h=t&u.F,v=t&u.G,y=t&u.S,g=t&u.P,m=t&u.B,b=v?r:y?r[e]||(r[e]={}):(r[e]||{})[s],_=v?o:o[e]||(o[e]={}),w=_[s]||(_[s]={});for(f in v&&(n=e),n)l=!h&&b&&void 0!==b[f],p=(l?b:n)[f],d=m&&l?c(p,r):g&&"function"==typeof p?c(Function.call,p):p,b&&a(b,f,p,t&u.U),_[f]!=p&&i(_,f,d),g&&w[f]!=p&&(w[f]=p)};r.core=o,u.F=1,u.G=2,u.S=4,u.P=8,u.B=16,u.W=32,u.U=64,u.R=128,t.exports=u},"5d73":function(t,e,n){t.exports=n("469f")},"5f1b":function(t,e,n){"use strict";var r=n("23c6"),o=RegExp.prototype.exec;t.exports=function(t,e){var n=t.exec;if("function"===typeof n){var i=n.call(t,e);if("object"!==typeof i)throw new TypeError("RegExp exec method returned something other than an Object or null");return i}if("RegExp"!==r(t))throw new TypeError("RegExp#exec called on incompatible receiver");return o.call(t,e)}},"626a":function(t,e,n){var r=n("2d95");t.exports=Object("z").propertyIsEnumerable(0)?Object:function(t){return"String"==r(t)?t.split(""):Object(t)}},"62a0":function(t,e){var n=0,r=Math.random();t.exports=function(t){return"Symbol(".concat(void 0===t?"":t,")_",(++n+r).toString(36))}},"63b6":function(t,e,n){var r=n("e53d"),o=n("584a"),i=n("d864"),a=n("35e8"),c=n("07e3"),s="prototype",u=function(t,e,n){var f,l,p,d=t&u.F,h=t&u.G,v=t&u.S,y=t&u.P,g=t&u.B,m=t&u.W,b=h?o:o[e]||(o[e]={}),_=b[s],w=h?r:v?r[e]:(r[e]||{})[s];for(f in h&&(n=e),n)l=!d&&w&&void 0!==w[f],l&&c(b,f)||(p=l?w[f]:n[f],b[f]=h&&"function"!=typeof w[f]?n[f]:g&&l?i(p,r):m&&w[f]==p?function(t){var e=function(e,n,r){if(this instanceof t){switch(arguments.length){case 0:return new t;case 1:return new t(e);case 2:return new t(e,n)}return new t(e,n,r)}return t.apply(this,arguments)};return e[s]=t[s],e}(p):y&&"function"==typeof p?i(Function.call,p):p,y&&((b.virtual||(b.virtual={}))[f]=p,t&u.R&&_&&!_[f]&&a(_,f,p)))};u.F=1,u.G=2,u.S=4,u.P=8,u.B=16,u.W=32,u.U=64,u.R=128,t.exports=u},6762:function(t,e,n){"use strict";var r=n("5ca1"),o=n("c366")(!0);r(r.P,"Array",{includes:function(t){return o(this,t,arguments.length>1?arguments[1]:void 0)}}),n("9c6c")("includes")},6821:function(t,e,n){var r=n("626a"),o=n("be13");t.exports=function(t){return r(o(t))}},"69a8":function(t,e){var n={}.hasOwnProperty;t.exports=function(t,e){return n.call(t,e)}},"6a99":function(t,e,n){var r=n("d3f4");t.exports=function(t,e){if(!r(t))return t;var n,o;if(e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;if("function"==typeof(n=t.valueOf)&&!r(o=n.call(t)))return o;if(!e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;throw TypeError("Can't convert object to primitive value")}},"6b4c":function(t,e){var n={}.toString;t.exports=function(t){return n.call(t).slice(8,-1)}},"6c1c":function(t,e,n){n("c367");for(var r=n("e53d"),o=n("35e8"),i=n("481b"),a=n("5168")("toStringTag"),c="CSSRuleList,CSSStyleDeclaration,CSSValueList,ClientRectList,DOMRectList,DOMStringList,DOMTokenList,DataTransferItemList,FileList,HTMLAllCollection,HTMLCollection,HTMLFormElement,HTMLSelectElement,MediaList,MimeTypeArray,NamedNodeMap,NodeList,PaintRequestList,Plugin,PluginArray,SVGLengthList,SVGNumberList,SVGPathSegList,SVGPointList,SVGStringList,SVGTransformList,SourceBufferList,StyleSheetList,TextTrackCueList,TextTrackList,TouchList".split(","),s=0;s=u?t?"":void 0:(i=c.charCodeAt(s),i<55296||i>56319||s+1===u||(a=c.charCodeAt(s+1))<56320||a>57343?t?c.charAt(s):i:t?c.slice(s,s+2):a-56320+(i-55296<<10)+65536)}}},7726:function(t,e){var n=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},"774e":function(t,e,n){t.exports=n("d2d5")},"77f1":function(t,e,n){var r=n("4588"),o=Math.max,i=Math.min;t.exports=function(t,e){return t=r(t),t<0?o(t+e,0):i(t,e)}},"794b":function(t,e,n){t.exports=!n("8e60")&&!n("294c")(function(){return 7!=Object.defineProperty(n("1ec9")("div"),"a",{get:function(){return 7}}).a})},"79aa":function(t,e){t.exports=function(t){if("function"!=typeof t)throw TypeError(t+" is not a function!");return t}},"79e5":function(t,e){t.exports=function(t){try{return!!t()}catch(e){return!0}}},"7cd6":function(t,e,n){var r=n("40c3"),o=n("5168")("iterator"),i=n("481b");t.exports=n("584a").getIteratorMethod=function(t){if(void 0!=t)return t[o]||t["@@iterator"]||i[r(t)]}},"7d7b":function(t,e,n){var r=n("e4ae"),o=n("7cd6");t.exports=n("584a").getIterator=function(t){var e=o(t);if("function"!=typeof e)throw TypeError(t+" is not iterable!");return r(e.call(t))}},"7e90":function(t,e,n){var r=n("d9f6"),o=n("e4ae"),i=n("c3a1");t.exports=n("8e60")?Object.defineProperties:function(t,e){o(t);var n,a=i(e),c=a.length,s=0;while(c>s)r.f(t,n=a[s++],e[n]);return t}},8378:function(t,e){var n=t.exports={version:"2.6.5"};"number"==typeof __e&&(__e=n)},8436:function(t,e){t.exports=function(){}},"86cc":function(t,e,n){var r=n("cb7c"),o=n("c69a"),i=n("6a99"),a=Object.defineProperty;e.f=n("9e1e")?Object.defineProperty:function(t,e,n){if(r(t),e=i(e,!0),r(n),o)try{return a(t,e,n)}catch(c){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(t[e]=n.value),t}},"8aae":function(t,e,n){n("32a6"),t.exports=n("584a").Object.keys},"8e60":function(t,e,n){t.exports=!n("294c")(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},"8f60":function(t,e,n){"use strict";var r=n("a159"),o=n("aebd"),i=n("45f2"),a={};n("35e8")(a,n("5168")("iterator"),function(){return this}),t.exports=function(t,e,n){t.prototype=r(a,{next:o(1,n)}),i(t,e+" Iterator")}},9003:function(t,e,n){var r=n("6b4c");t.exports=Array.isArray||function(t){return"Array"==r(t)}},9138:function(t,e,n){t.exports=n("35e8")},9306:function(t,e,n){"use strict";var r=n("c3a1"),o=n("9aa9"),i=n("355d"),a=n("241e"),c=n("335c"),s=Object.assign;t.exports=!s||n("294c")(function(){var t={},e={},n=Symbol(),r="abcdefghijklmnopqrst";return t[n]=7,r.split("").forEach(function(t){e[t]=t}),7!=s({},t)[n]||Object.keys(s({},e)).join("")!=r})?function(t,e){var n=a(t),s=arguments.length,u=1,f=o.f,l=i.f;while(s>u){var p,d=c(arguments[u++]),h=f?r(d).concat(f(d)):r(d),v=h.length,y=0;while(v>y)l.call(d,p=h[y++])&&(n[p]=d[p])}return n}:s},9427:function(t,e,n){var r=n("63b6");r(r.S,"Object",{create:n("a159")})},"95d5":function(t,e,n){var r=n("40c3"),o=n("5168")("iterator"),i=n("481b");t.exports=n("584a").isIterable=function(t){var e=Object(t);return void 0!==e[o]||"@@iterator"in e||i.hasOwnProperty(r(e))}},"9aa9":function(t,e){e.f=Object.getOwnPropertySymbols},"9b43":function(t,e,n){var r=n("d8e8");t.exports=function(t,e,n){if(r(t),void 0===e)return t;switch(n){case 1:return function(n){return t.call(e,n)};case 2:return function(n,r){return t.call(e,n,r)};case 3:return function(n,r,o){return t.call(e,n,r,o)}}return function(){return t.apply(e,arguments)}}},"9c6c":function(t,e,n){var r=n("2b4c")("unscopables"),o=Array.prototype;void 0==o[r]&&n("32e9")(o,r,{}),t.exports=function(t){o[r][t]=!0}},"9def":function(t,e,n){var r=n("4588"),o=Math.min;t.exports=function(t){return t>0?o(r(t),9007199254740991):0}},"9e1e":function(t,e,n){t.exports=!n("79e5")(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},a159:function(t,e,n){var r=n("e4ae"),o=n("7e90"),i=n("1691"),a=n("5559")("IE_PROTO"),c=function(){},s="prototype",u=function(){var t,e=n("1ec9")("iframe"),r=i.length,o="<",a=">";e.style.display="none",n("32fc").appendChild(e),e.src="javascript:",t=e.contentWindow.document,t.open(),t.write(o+"script"+a+"document.F=Object"+o+"/script"+a),t.close(),u=t.F;while(r--)delete u[s][i[r]];return u()};t.exports=Object.create||function(t,e){var n;return null!==t?(c[s]=r(t),n=new c,c[s]=null,n[a]=t):n=u(),void 0===e?n:o(n,e)}},a352:function(e,n){e.exports=t},a3c3:function(t,e,n){var r=n("63b6");r(r.S+r.F,"Object",{assign:n("9306")})},a481:function(t,e,n){"use strict";var r=n("cb7c"),o=n("4bf8"),i=n("9def"),a=n("4588"),c=n("0390"),s=n("5f1b"),u=Math.max,f=Math.min,l=Math.floor,p=/\$([$&`']|\d\d?|<[^>]*>)/g,d=/\$([$&`']|\d\d?)/g,h=function(t){return void 0===t?t:String(t)};n("214f")("replace",2,function(t,e,n,v){return[function(r,o){var i=t(this),a=void 0==r?void 0:r[e];return void 0!==a?a.call(r,i,o):n.call(String(i),r,o)},function(t,e){var o=v(n,t,this,e);if(o.done)return o.value;var l=r(t),p=String(this),d="function"===typeof e;d||(e=String(e));var g=l.global;if(g){var m=l.unicode;l.lastIndex=0}var b=[];while(1){var _=s(l,p);if(null===_)break;if(b.push(_),!g)break;var w=String(_[0]);""===w&&(l.lastIndex=c(p,i(l.lastIndex),m))}for(var x="",O=0,S=0;S=O&&(x+=p.slice(O,A)+j,O=A+$.length)}return x+p.slice(O)}];function y(t,e,r,i,a,c){var s=r+t.length,u=i.length,f=d;return void 0!==a&&(a=o(a),f=p),n.call(c,f,function(n,o){var c;switch(o.charAt(0)){case"$":return"$";case"&":return t;case"`":return e.slice(0,r);case"'":return e.slice(s);case"<":c=a[o.slice(1,-1)];break;default:var f=+o;if(0===f)return n;if(f>u){var p=l(f/10);return 0===p?n:p<=u?void 0===i[p-1]?o.charAt(1):i[p-1]+o.charAt(1):n}c=i[f-1]}return void 0===c?"":c})}})},a4bb:function(t,e,n){t.exports=n("8aae")},a745:function(t,e,n){t.exports=n("f410")},aae3:function(t,e,n){var r=n("d3f4"),o=n("2d95"),i=n("2b4c")("match");t.exports=function(t){var e;return r(t)&&(void 0!==(e=t[i])?!!e:"RegExp"==o(t))}},aebd:function(t,e){t.exports=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}}},b0c5:function(t,e,n){"use strict";var r=n("520a");n("5ca1")({target:"RegExp",proto:!0,forced:r!==/./.exec},{exec:r})},b0dc:function(t,e,n){var r=n("e4ae");t.exports=function(t,e,n,o){try{return o?e(r(n)[0],n[1]):e(n)}catch(a){var i=t["return"];throw void 0!==i&&r(i.call(t)),a}}},b447:function(t,e,n){var r=n("3a38"),o=Math.min;t.exports=function(t){return t>0?o(r(t),9007199254740991):0}},b8e3:function(t,e){t.exports=!0},be13:function(t,e){t.exports=function(t){if(void 0==t)throw TypeError("Can't call method on "+t);return t}},c366:function(t,e,n){var r=n("6821"),o=n("9def"),i=n("77f1");t.exports=function(t){return function(e,n,a){var c,s=r(e),u=o(s.length),f=i(a,u);if(t&&n!=n){while(u>f)if(c=s[f++],c!=c)return!0}else for(;u>f;f++)if((t||f in s)&&s[f]===n)return t||f||0;return!t&&-1}}},c367:function(t,e,n){"use strict";var r=n("8436"),o=n("50ed"),i=n("481b"),a=n("36c3");t.exports=n("30f1")(Array,"Array",function(t,e){this._t=a(t),this._i=0,this._k=e},function(){var t=this._t,e=this._k,n=this._i++;return!t||n>=t.length?(this._t=void 0,o(1)):o(0,"keys"==e?n:"values"==e?t[n]:[n,t[n]])},"values"),i.Arguments=i.Array,r("keys"),r("values"),r("entries")},c3a1:function(t,e,n){var r=n("e6f3"),o=n("1691");t.exports=Object.keys||function(t){return r(t,o)}},c649:function(t,e,n){"use strict";(function(t){n.d(e,"c",function(){return l}),n.d(e,"a",function(){return u}),n.d(e,"b",function(){return a}),n.d(e,"d",function(){return f}),n("a481");var r=n("4aa6"),o=n.n(r);function i(){return"undefined"!==typeof window?window.console:t.console}var a=i();function c(t){var e=o()(null);return function(n){var r=e[n];return r||(e[n]=t(n))}}var s=/-(\w)/g,u=c(function(t){return t.replace(s,function(t,e){return e?e.toUpperCase():""})});function f(t){null!==t.parentElement&&t.parentElement.removeChild(t)}function l(t,e,n){var r=0===n?t.children[0]:t.children[n-1].nextSibling;t.insertBefore(e,r)}}).call(this,n("c8ba"))},c69a:function(t,e,n){t.exports=!n("9e1e")&&!n("79e5")(function(){return 7!=Object.defineProperty(n("230e")("div"),"a",{get:function(){return 7}}).a})},c8ba:function(t,e){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(r){"object"===typeof window&&(n=window)}t.exports=n},c8bb:function(t,e,n){t.exports=n("54a1")},ca5a:function(t,e){var n=0,r=Math.random();t.exports=function(t){return"Symbol(".concat(void 0===t?"":t,")_",(++n+r).toString(36))}},cb7c:function(t,e,n){var r=n("d3f4");t.exports=function(t){if(!r(t))throw TypeError(t+" is not an object!");return t}},ce7e:function(t,e,n){var r=n("63b6"),o=n("584a"),i=n("294c");t.exports=function(t,e){var n=(o.Object||{})[t]||Object[t],a={};a[t]=e(n),r(r.S+r.F*i(function(){n(1)}),"Object",a)}},d2c8:function(t,e,n){var r=n("aae3"),o=n("be13");t.exports=function(t,e,n){if(r(e))throw TypeError("String#"+n+" doesn't accept regex!");return String(o(t))}},d2d5:function(t,e,n){n("1654"),n("549b"),t.exports=n("584a").Array.from},d3f4:function(t,e){t.exports=function(t){return"object"===typeof t?null!==t:"function"===typeof t}},d864:function(t,e,n){var r=n("79aa");t.exports=function(t,e,n){if(r(t),void 0===e)return t;switch(n){case 1:return function(n){return t.call(e,n)};case 2:return function(n,r){return t.call(e,n,r)};case 3:return function(n,r,o){return t.call(e,n,r,o)}}return function(){return t.apply(e,arguments)}}},d8e8:function(t,e){t.exports=function(t){if("function"!=typeof t)throw TypeError(t+" is not a function!");return t}},d9f6:function(t,e,n){var r=n("e4ae"),o=n("794b"),i=n("1bc3"),a=Object.defineProperty;e.f=n("8e60")?Object.defineProperty:function(t,e,n){if(r(t),e=i(e,!0),r(n),o)try{return a(t,e,n)}catch(c){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(t[e]=n.value),t}},dbdb:function(t,e,n){var r=n("584a"),o=n("e53d"),i="__core-js_shared__",a=o[i]||(o[i]={});(t.exports=function(t,e){return a[t]||(a[t]=void 0!==e?e:{})})("versions",[]).push({version:r.version,mode:n("b8e3")?"pure":"global",copyright:"© 2019 Denis Pushkarev (zloirock.ru)"})},dc62:function(t,e,n){n("9427");var r=n("584a").Object;t.exports=function(t,e){return r.create(t,e)}},e4ae:function(t,e,n){var r=n("f772");t.exports=function(t){if(!r(t))throw TypeError(t+" is not an object!");return t}},e53d:function(t,e){var n=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},e6f3:function(t,e,n){var r=n("07e3"),o=n("36c3"),i=n("5b4e")(!1),a=n("5559")("IE_PROTO");t.exports=function(t,e){var n,c=o(t),s=0,u=[];for(n in c)n!=a&&r(c,n)&&u.push(n);while(e.length>s)r(c,n=e[s++])&&(~i(u,n)||u.push(n));return u}},f410:function(t,e,n){n("1af6"),t.exports=n("584a").Array.isArray},f559:function(t,e,n){"use strict";var r=n("5ca1"),o=n("9def"),i=n("d2c8"),a="startsWith",c=""[a];r(r.P+r.F*n("5147")(a),"String",{startsWith:function(t){var e=i(this,t,a),n=o(Math.min(arguments.length>1?arguments[1]:void 0,e.length)),r=String(t);return c?c.call(e,r,n):e.slice(n,n+r.length)===r}})},f772:function(t,e){t.exports=function(t){return"object"===typeof t?null!==t:"function"===typeof t}},fa5b:function(t,e,n){t.exports=n("5537")("native-function-to-string",Function.toString)},fb15:function(t,e,n){"use strict";var r;n.r(e),"undefined"!==typeof window&&(r=window.document.currentScript)&&(r=r.src.match(/(.+\/)[^\/]+\.js(\?.*)?$/))&&(n.p=r[1]);var o=n("5176"),i=n.n(o),a=(n("f559"),n("a4bb")),c=n.n(a),s=(n("6762"),n("2fdb"),n("a745")),u=n.n(s);function f(t){if(u()(t))return t}var l=n("5d73"),p=n.n(l);function d(t,e){var n=[],r=!0,o=!1,i=void 0;try{for(var a,c=p()(t);!(r=(a=c.next()).done);r=!0)if(n.push(a.value),e&&n.length===e)break}catch(s){o=!0,i=s}finally{try{r||null==c["return"]||c["return"]()}finally{if(o)throw i}}return n}function h(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}function v(t,e){return f(t)||d(t,e)||h()}function y(t){if(u()(t)){for(var e=0,n=new Array(t.length);e=i?o.length:o.indexOf(t)});return n?a.filter(function(t){return-1!==t}):a}function M(t,e){var n=this;this.$nextTick(function(){return n.$emit(t.toLowerCase(),e)})}function j(t){var e=this;return function(n){null!==e.realList&&e["onDrag"+t](n),M.call(e,t,n)}}function T(t){if(!t||1!==t.length)return!1;var e=v(t,1),n=e[0].componentOptions;return!!n&&["transition-group","TransitionGroup"].includes(n.tag)}function P(t,e){var n=e.header,r=e.footer,o=0,i=0;return n&&(o=n.length,t=t?[].concat(O(n),O(t)):O(n)),r&&(i=r.length,t=t?[].concat(O(t),O(r)):O(r)),{children:t,headerOffset:o,footerOffset:i}}function D(t,e){var n=null,r=function(t,e){n=C(n,t,e)},o=c()(t).filter(function(t){return"id"===t||t.startsWith("data-")}).reduce(function(e,n){return e[n]=t[n],e},{});if(r("attrs",o),!e)return n;var a=e.on,s=e.props,u=e.attrs;return r("on",a),r("props",s),i()(n.attrs,u),n}var L=["Start","Add","Remove","Update","End"],I=["Choose","Sort","Filter","Clone"],N=["Move"].concat(L,I).map(function(t){return"on"+t}),R=null,F={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:function(t){return t}},element:{type:String,default:"div"},tag:{type:String,default:null},move:{type:Function,default:null},componentData:{type:Object,required:!1,default:null}},H={name:"draggable",inheritAttrs:!1,props:F,data:function(){return{transitionMode:!1,noneFunctionalComponentMode:!1,init:!1}},render:function(t){var e=this.$slots.default;this.transitionMode=T(e);var n=P(e,this.$slots),r=n.children,o=n.headerOffset,i=n.footerOffset;this.headerOffset=o,this.footerOffset=i;var a=D(this.$attrs,this.componentData);return t(this.getTag(),a,r)},created:function(){null!==this.list&&null!==this.value&&A["b"].error("Value and list props are mutually exclusive! Please set one or another."),"div"!==this.element&&A["b"].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&&A["b"].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:function(){var t=this;if(this.noneFunctionalComponentMode=this.getTag().toLowerCase()!==this.$el.nodeName.toLowerCase(),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: ".concat(this.getTag()));var e={};L.forEach(function(n){e["on"+n]=j.call(t,n)}),I.forEach(function(n){e["on"+n]=M.bind(t,n)});var n=c()(this.$attrs).reduce(function(e,n){return e[Object(A["a"])(n)]=t.$attrs[n],e},{}),r=i()({},this.options,n,e,{onMove:function(e,n){return t.onDragMove(e,n)}});!("draggable"in r)&&(r.draggable=">*"),this._sortable=new $.a(this.rootContainer,r),this.computeIndexes()},beforeDestroy:function(){void 0!==this._sortable&&this._sortable.destroy()},computed:{rootContainer:function(){return this.transitionMode?this.$el.children[0]:this.$el},realList:function(){return this.list?this.list:this.value}},watch:{options:{handler:function(t){this.updateOptions(t)},deep:!0},$attrs:{handler:function(t){this.updateOptions(t)},deep:!0},realList:function(){this.computeIndexes()}},methods:{getTag:function(){return this.tag||this.element},updateOptions:function(t){for(var e in t){var n=Object(A["a"])(e);-1===N.indexOf(n)&&this._sortable.option(n,t[e])}},getChildrenNodes:function(){if(this.init||(this.noneFunctionalComponentMode=this.noneFunctionalComponentMode&&1===this.$children.length,this.init=!0),this.noneFunctionalComponentMode)return this.$children[0].$slots.default;var t=this.$slots.default;return this.transitionMode?t[0].child.$slots.default:t},computeIndexes:function(){var t=this;this.$nextTick(function(){t.visibleIndexes=E(t.getChildrenNodes(),t.rootContainer.children,t.transitionMode,t.footerOffset)})},getUnderlyingVm:function(t){var e=k(this.getChildrenNodes()||[],t);if(-1===e)return null;var n=this.realList[e];return{index:e,element:n}},getUnderlyingPotencialDraggableComponent:function(t){var e=t.__vue__;return e&&e.$options&&"transition-group"===e.$options._componentTag?e.$parent:e},emitChanges:function(t){var e=this;this.$nextTick(function(){e.$emit("change",t)})},alterList:function(t){if(this.list)t(this.list);else{var e=O(this.value);t(e),this.$emit("input",e)}},spliceList:function(){var t=arguments,e=function(e){return e.splice.apply(e,O(t))};this.alterList(e)},updatePosition:function(t,e){var n=function(n){return n.splice(e,0,n.splice(t,1)[0])};this.alterList(n)},getRelatedContextFromMoveEvent:function(t){var e=t.to,n=t.related,r=this.getUnderlyingPotencialDraggableComponent(e);if(!r)return{component:r};var o=r.realList,a={list:o,component:r};if(e!==n&&o&&r.getUnderlyingVm){var c=r.getUnderlyingVm(n);if(c)return i()(c,a)}return a},getVmIndex:function(t){var e=this.visibleIndexes,n=e.length;return t>n-1?n:e[t]},getComponent:function(){return this.$slots.default[0].componentInstance},resetTransitionData:function(t){if(this.noTransitionOnDrag&&this.transitionMode){var e=this.getChildrenNodes();e[t].data=null;var n=this.getComponent();n.children=[],n.kept=void 0}},onDragStart:function(t){this.context=this.getUnderlyingVm(t.item),t.item._underlying_vm_=this.clone(this.context.element),R=t.item},onDragAdd:function(t){var e=t.item._underlying_vm_;if(void 0!==e){Object(A["d"])(t.item);var n=this.getVmIndex(t.newIndex);this.spliceList(n,0,e),this.computeIndexes();var r={element:e,newIndex:n};this.emitChanges({added:r})}},onDragRemove:function(t){if(Object(A["c"])(this.rootContainer,t.item,t.oldIndex),"clone"!==t.pullMode){var e=this.context.index;this.spliceList(e,1);var n={element:this.context.element,oldIndex:e};this.resetTransitionData(e),this.emitChanges({removed:n})}else Object(A["d"])(t.clone)},onDragUpdate:function(t){Object(A["d"])(t.item),Object(A["c"])(t.from,t.item,t.oldIndex);var e=this.context.index,n=this.getVmIndex(t.newIndex);this.updatePosition(e,n);var r={element:this.context.element,oldIndex:e,newIndex:n};this.emitChanges({moved:r})},updateProperty:function(t,e){t.hasOwnProperty(e)&&(t[e]+=this.headerOffset)},computeFutureIndex:function(t,e){if(!t.element)return 0;var n=O(e.to.children).filter(function(t){return"none"!==t.style["display"]}),r=n.indexOf(e.related),o=t.component.getVmIndex(r),i=-1!==n.indexOf(R);return i||!e.willInsertAfter?o:o+1},onDragMove:function(t,e){var n=this.move;if(!n||!this.realList)return!0;var r=this.getRelatedContextFromMoveEvent(t),o=this.context,a=this.computeFutureIndex(r,t);i()(o,{futureIndex:a});var c=i()({},t,{relatedContext:r,draggedContext:o});return n(c,e)},onDragEnd:function(){this.computeIndexes(),R=null}}};"undefined"!==typeof window&&"Vue"in window&&window.Vue.component("draggable",H);var z=H;e["default"]=z}})["default"]})},"19e9":function(t,e,n){var r,o,i; +/*! + autosize 4.0.2 + license: MIT + http://www.jacklmoore.com/autosize +*/ +/*! + autosize 4.0.2 + license: MIT + http://www.jacklmoore.com/autosize +*/ +(function(n,a){o=[t,e],r=a,i="function"===typeof r?r.apply(e,o):r,void 0===i||(t.exports=i)})(0,function(t,e){"use strict";var n="function"===typeof Map?new Map:function(){var t=[],e=[];return{has:function(e){return t.indexOf(e)>-1},get:function(n){return e[t.indexOf(n)]},set:function(n,r){-1===t.indexOf(n)&&(t.push(n),e.push(r))},delete:function(n){var r=t.indexOf(n);r>-1&&(t.splice(r,1),e.splice(r,1))}}}(),r=function(t){return new Event(t,{bubbles:!0})};try{new Event("test")}catch(s){r=function(t){var e=document.createEvent("Event");return e.initEvent(t,!0,!1),e}}function o(t){if(t&&t.nodeName&&"TEXTAREA"===t.nodeName&&!n.has(t)){var e=null,o=null,i=null,a=function(){t.clientWidth!==o&&p()},c=function(e){window.removeEventListener("resize",a,!1),t.removeEventListener("input",p,!1),t.removeEventListener("keyup",p,!1),t.removeEventListener("autosize:destroy",c,!1),t.removeEventListener("autosize:update",p,!1),Object.keys(e).forEach(function(n){t.style[n]=e[n]}),n.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",c,!1),"onpropertychange"in t&&"oninput"in t&&t.addEventListener("keyup",p,!1),window.addEventListener("resize",a,!1),t.addEventListener("input",p,!1),t.addEventListener("autosize:update",p,!1),t.style.overflowX="hidden",t.style.wordWrap="break-word",n.set(t,{destroy:c,update:p}),s()}function s(){var n=window.getComputedStyle(t,null);"vertical"===n.resize?t.style.resize="none":"both"===n.resize&&(t.style.resize="horizontal"),e="content-box"===n.boxSizing?-(parseFloat(n.paddingTop)+parseFloat(n.paddingBottom)):parseFloat(n.borderTopWidth)+parseFloat(n.borderBottomWidth),isNaN(e)&&(e=0),p()}function u(e){var n=t.style.width;t.style.width="0px",t.offsetWidth,t.style.width=n,t.style.overflowY=e}function f(t){var e=[];while(t&&t.parentNode&&t.parentNode instanceof Element)t.parentNode.scrollTop&&e.push({node:t.parentNode,scrollTop:t.parentNode.scrollTop}),t=t.parentNode;return e}function l(){if(0!==t.scrollHeight){var n=f(t),r=document.documentElement&&document.documentElement.scrollTop;t.style.height="",t.style.height=t.scrollHeight+e+"px",o=t.clientWidth,n.forEach(function(t){t.node.scrollTop=t.scrollTop}),r&&(document.documentElement.scrollTop=r)}}function p(){l();var e=Math.round(parseFloat(t.style.height)),n=window.getComputedStyle(t,null),o="content-box"===n.boxSizing?Math.round(parseFloat(n.height)):t.offsetHeight;if(on)e.push(arguments[n++]);return g[++y]=function(){c("function"==typeof t?t:Function(t),e)},r(y),y},d=function(t){delete g[t]},"process"==n("71fa")(l)?r=function(t){l.nextTick(a(b,t,1))}:v&&v.now?r=function(t){v.now(a(b,t,1))}:h?(o=new h,i=o.port2,o.port1.onmessage=_,r=a(i.postMessage,i,1)):f.addEventListener&&"function"==typeof postMessage&&!f.importScripts?(r=function(t){f.postMessage(t+"","*")},f.addEventListener("message",_,!1)):r=m in u("script")?function(t){s.appendChild(u("script"))[m]=function(){s.removeChild(this),b.call(t)}}:function(t){setTimeout(a(b,t,1),0)}),t.exports={set:p,clear:d}},"1b55":function(t,e,n){var r=n("7772")("wks"),o=n("7b00"),i=n("da3c").Symbol,a="function"==typeof i,c=t.exports=function(t){return r[t]||(r[t]=a&&i[t]||(a?i:o)("Symbol."+t))};c.store=r},"1b8f":function(t,e,n){var r=n("a812"),o=Math.max,i=Math.min;t.exports=function(t,e){return t=r(t),t<0?o(t+e,0):i(t,e)}},"1be4":function(t,e,n){"use strict";var r=n("da3c"),o=n("a7d3"),i=n("3adc"),a=n("7d95"),c=n("1b55")("species");t.exports=function(t){var e="function"==typeof o[t]?o[t]:r[t];a&&e&&!e[c]&&i.f(e,c,{configurable:!0,get:function(){return this}})}},"1c01":function(t,e,n){var r=n("a7d3"),o=r.JSON||(r.JSON={stringify:JSON.stringify});t.exports=function(t){return o.stringify.apply(o,arguments)}},"1c08":function(t,e,n){var r=n("27b2"),o=n("ab4c"),i=n("cc20");t.exports=function(t,e){if(r(t),o(e)&&e.constructor===t)return e;var n=i.f(t),a=n.resolve;return a(e),n.promise}},"1d27":function(t,e,n){var r=n("27b2");t.exports=function(t,e,n,o){try{return o?e(r(n)[0],n[1]):e(n)}catch(a){var i=t["return"];throw void 0!==i&&r(i.call(t)),a}}},"1d73":function(t,e,n){t.exports=n("312a")("native-function-to-string",Function.toString)},"1dce":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.withParams=e.validationMixin=e.Vuelidate=void 0;var r=Object.assign||function(t){for(var e=1;e1?a:a.$sub[0]:null;return{output:o,params:c}}},computed:{run:function(){return this.runRule(this.lazyParentModel())},$params:function(){return this.run.params},proxy:function(){var t=this.run.output;return t[p]?!!t.v:!!t},$pending:function(){var t=this.run.output;return!!t[p]&&t.p}}}),s=e.extend({data:function(){return{dirty:!1,validations:null,lazyModel:null,model:null,prop:null,lazyParentModel:null,rootModel:null}},methods:r({},y,{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:r({},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=c(this.keys,function(e){return{enumerable:!0,configurable:!0,get:function(){return t.refProxy(e)}}}),n=c(g,function(e){return{enumerable:!0,configurable:!0,get:function(){return t[e]}}}),o=c(m,function(e){return{enumerable:!1,configurable:!0,get:function(){return t[e]}}});return Object.defineProperties({},r({},e,n,o))},children:function(){var t=this;return[].concat(this.nestedKeys.map(function(e){return w(t,e)}),this.ruleKeys.map(function(e){return x(t,e)})).filter(Boolean)}})}),v=s.extend({methods:{isNested:function(t){return"undefined"!==typeof this.validations[t]()},getRef:function(t){var e=this;return{get proxy(){return e.validations[t]()||!1}}}}}),_=s.extend({computed:{keys:function(){var t=this.getModel();return u(t)?Object.keys(t):[]},tracker:function(){var t=this,e=this.validations.$trackBy;return e?function(n){return""+l(t.rootModel,t.getModelKey(n),e)}:function(t){return""+t}},eagerParentModel:function(){var t=this.lazyParentModel();return function(){return t}},children:function(){var t=this,e=this.validations,n=this.getModel(),i=r({},e);delete i["$trackBy"];var a={};return this.keys.map(function(e){var r=t.tracker(e);return a.hasOwnProperty(r)?null:(a[r]=!0,(0,o.h)(s,r,{validations:i,prop:e,lazyParentModel:t.eagerParentModel,model:n[e],rootModel:t.rootModel}))}).filter(Boolean)}},methods:{isNested:function(){return!0},getRef:function(t){return this.refs[this.tracker(t)]}}}),w=function(t,e){if("$each"===e)return(0,o.h)(_,e,{validations:t.validations[e],lazyParentModel:t.lazyParentModel,prop:e,lazyModel:t.getModel,rootModel:t.rootModel});var n=t.validations[e];if(Array.isArray(n)){var r=t.rootModel,i=c(n,function(t){return function(){return l(r,r.$v,t)}},function(t){return Array.isArray(t)?t.join("."):t});return(0,o.h)(v,e,{validations:i,lazyParentModel:a,prop:e,lazyModel:a,rootModel:r})}return(0,o.h)(s,e,{validations:n,lazyParentModel:t.getModel,prop:e,lazyModel:t.getModelKey,rootModel:t.rootModel})},x=function(t,e){return(0,o.h)(n,e,{rule:t.validations[e],lazyParentModel:t.lazyParentModel,lazyModel:t.getModel,rootModel:t.rootModel})};return b={VBase:e,Validation:s},b},w=null;function x(t){if(w)return w;var e=t.constructor;while(e.super)e=e.super;return w=e,e}var O=function(t,e){var n=x(t),r=_(n),i=r.Validation,c=r.VBase,s=new c({computed:{children:function(){var n="function"===typeof e?e.call(t):e;return[(0,o.h)(i,"$v",{validations:n,lazyParentModel:a,prop:"$v",model:t,rootModel:t})]}}});return s},S={data:function(){var t=this.$options.validations;return t&&(this._vuelidate=O(this,t)),{}},beforeCreate:function(){var t=this.$options,e=t.validations;e&&(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(S)}e.Vuelidate=$,e.validationMixin=S,e.withParams=i.withParams,e.default=$},"1f51":function(t,e,n){var r=n("b808"),o=n("a0a8"),i=n("0f4a"),a=n("c0f4")("src"),c=n("1d73"),s="toString",u=(""+c).split(s);n("ca38").inspectSource=function(t){return c.call(t)},(t.exports=function(t,e,n,c){var s="function"==typeof n;s&&(i(n,"name")||o(n,"name",e)),t[e]!==n&&(s&&(i(n,a)||o(n,a,t[e]?""+t[e]:u.join(String(e)))),t===r?t[e]=n:c?t[e]?t[e]=n:o(t,e,n):(delete t[e],o(t,e,n)))})(Function.prototype,s,function(){return"function"==typeof this&&this[a]||c.call(this)})},"20d6":function(t,e,n){"use strict";var r=n("5ca1"),o=n("0a49")(6),i="findIndex",a=!0;i in[]&&Array(1)[i](function(){a=!1}),r(r.P+r.F*a,"Array",{findIndex:function(t){return o(this,t,arguments.length>1?arguments[1]:void 0)}}),n("9c6c")(i)},"214f":function(t,e,n){"use strict";n("b0c5");var r=n("2aba"),o=n("32e9"),i=n("79e5"),a=n("be13"),c=n("2b4c"),s=n("520a"),u=c("species"),f=!i(function(){var t=/./;return t.exec=function(){var t=[];return t.groups={a:"7"},t},"7"!=="".replace(t,"$")}),l=function(){var t=/(?:)/,e=t.exec;t.exec=function(){return e.apply(this,arguments)};var n="ab".split(t);return 2===n.length&&"a"===n[0]&&"b"===n[1]}();t.exports=function(t,e,n){var p=c(t),d=!i(function(){var e={};return e[p]=function(){return 7},7!=""[t](e)}),h=d?!i(function(){var e=!1,n=/a/;return n.exec=function(){return e=!0,null},"split"===t&&(n.constructor={},n.constructor[u]=function(){return n}),n[p](""),!e}):void 0;if(!d||!h||"replace"===t&&!f||"split"===t&&!l){var v=/./[p],y=n(a,p,""[t],function(t,e,n,r,o){return e.exec===s?d&&!o?{done:!0,value:v.call(e,n,r)}:{done:!0,value:t.call(n,e,r)}:{done:!1}}),g=y[0],m=y[1];r(String.prototype,t,g),o(RegExp.prototype,p,2==e?function(t,e){return m.call(t,this,e)}:function(t){return m.call(t,this)})}}},2299:function(t,e,n){var r=n("b67f")("iterator"),o=!1;try{var i=[7][r]();i["return"]=function(){o=!0},Array.from(i,function(){throw 2})}catch(a){}t.exports=function(t,e){if(!e&&!o)return!1;var n=!1;try{var i=[7],c=i[r]();c.next=function(){return{done:n=!0}},i[r]=function(){return c},t(i)}catch(a){}return n}},"230e":function(t,e,n){var r=n("d3f4"),o=n("7726").document,i=r(o)&&r(o.createElement);t.exports=function(t){return i?o.createElement(t):{}}},2312:function(t,e,n){t.exports=n("8ce0")},"23c6":function(t,e,n){var r=n("2d95"),o=n("2b4c")("toStringTag"),i="Arguments"==r(function(){return arguments}()),a=function(t,e){try{return t[e]}catch(n){}};t.exports=function(t){var e,n,c;return void 0===t?"Undefined":null===t?"Null":"string"==typeof(n=a(e=Object(t),o))?n:i?r(e):"Object"==(c=r(e))&&"function"==typeof e.callee?"Arguments":c}},2418:function(t,e,n){var r=n("6a9b"),o=n("a5ab"),i=n("1b8f");t.exports=function(t){return function(e,n,a){var c,s=r(e),u=o(s.length),f=i(a,u);if(t&&n!=n){while(u>f)if(c=s[f++],c!=c)return!0}else for(;u>f;f++)if((t||f in s)&&s[f]===n)return t||f||0;return!t&&-1}}},"245b":function(t,e){t.exports=function(t,e){return{value:e,done:!!t}}},"268f":function(t,e,n){t.exports=n("2a04")},2695:function(t,e,n){var r=n("43c8"),o=n("6a9b"),i=n("2418")(!1),a=n("5d8f")("IE_PROTO");t.exports=function(t,e){var n,c=o(t),s=0,u=[];for(n in c)n!=a&&r(c,n)&&u.push(n);while(e.length>s)r(c,n=e[s++])&&(~i(u,n)||u.push(n));return u}},"27b2":function(t,e,n){var r=n("ab4c");t.exports=function(t){if(!r(t))throw TypeError(t+" is not an object!");return t}},2877:function(t,e,n){"use strict";function r(t,e,n,r,o,i,a,c){var s,u="function"===typeof t?t.options:t;if(e&&(u.render=e,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(s=function(t){t=t||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,t||"undefined"===typeof __VUE_SSR_CONTEXT__||(t=__VUE_SSR_CONTEXT__),o&&o.call(this,t),t&&t._registeredComponents&&t._registeredComponents.add(a)},u._ssrRegister=s):o&&(s=c?function(){o.call(this,this.$root.$options.shadowRoot)}:o),s)if(u.functional){u._injectStyles=s;var f=u.render;u.render=function(t,e){return s.call(e),f(t,e)}}else{var l=u.beforeCreate;u.beforeCreate=l?[].concat(l,s):[s]}return{exports:t,options:u}}n.d(e,"a",function(){return r})},"28a5":function(t,e,n){"use strict";var r=n("aae3"),o=n("cb7c"),i=n("ebd6"),a=n("0390"),c=n("9def"),s=n("5f1b"),u=n("520a"),f=Math.min,l=[].push,p="split",d="length",h="lastIndex",v=!!function(){try{return new RegExp("x","y")}catch(t){}}();n("214f")("split",2,function(t,e,n,y){var g=n;return"c"=="abbc"[p](/(b)*/)[1]||4!="test"[p](/(?:)/,-1)[d]||2!="ab"[p](/(?:ab)*/)[d]||4!="."[p](/(.?)(.?)/)[d]||"."[p](/()()/)[d]>1||""[p](/.?/)[d]?g=function(t,e){var o=String(this);if(void 0===t&&0===e)return[];if(!r(t))return n.call(o,t,e);var i,a,c,s=[],f=(t.ignoreCase?"i":"")+(t.multiline?"m":"")+(t.unicode?"u":"")+(t.sticky?"y":""),p=0,v=void 0===e?4294967295:e>>>0,y=new RegExp(t.source,f+"g");while(i=u.call(y,o)){if(a=y[h],a>p&&(s.push(o.slice(p,i.index)),i[d]>1&&i.index=v))break;y[h]===i.index&&y[h]++}return p===o[d]?!c&&y.test("")||s.push(""):s.push(o.slice(p)),s[d]>v?s.slice(0,v):s}:"0"[p](void 0,0)[d]&&(g=function(t,e){return void 0===t&&0===e?[]:n.call(this,t,e)}),[function(n,r){var o=t(this),i=void 0==n?void 0:n[e];return void 0!==i?i.call(n,o,r):g.call(String(o),n,r)},function(t,e){var r=y(g,t,this,e,g!==n);if(r.done)return r.value;var u=o(t),l=String(this),p=i(u,RegExp),d=u.unicode,h=(u.ignoreCase?"i":"")+(u.multiline?"m":"")+(u.unicode?"u":"")+(v?"y":"g"),m=new p(v?u:"^(?:"+u.source+")",h),b=void 0===e?4294967295:e>>>0;if(0===b)return[];if(0===l.length)return null===s(m,l)?[l]:[];var _=0,w=0,x=[];while(w=u?t?"":void 0:(i=c.charCodeAt(s),i<55296||i>56319||s+1===u||(a=c.charCodeAt(s+1))<56320||a>57343?t?c.charAt(s):i:t?c.slice(s,s+2):a-56320+(i-55296<<10)+65536)}}},"2aba":function(t,e,n){var r=n("7726"),o=n("32e9"),i=n("69a8"),a=n("ca5a")("src"),c="toString",s=Function[c],u=(""+s).split(c);n("8378").inspectSource=function(t){return s.call(t)},(t.exports=function(t,e,n,c){var s="function"==typeof n;s&&(i(n,"name")||o(n,"name",e)),t[e]!==n&&(s&&(i(n,a)||o(n,a,t[e]?""+t[e]:u.join(String(e)))),t===r?t[e]=n:c?t[e]?t[e]=n:o(t,e,n):(delete t[e],o(t,e,n)))})(Function.prototype,c,function(){return"function"==typeof this&&this[a]||s.call(this)})},"2aeb":function(t,e,n){var r=n("cb7c"),o=n("1495"),i=n("e11e"),a=n("613b")("IE_PROTO"),c=function(){},s="prototype",u=function(){var t,e=n("230e")("iframe"),r=i.length,o="<",a=">";e.style.display="none",n("fab2").appendChild(e),e.src="javascript:",t=e.contentWindow.document,t.open(),t.write(o+"script"+a+"document.F=Object"+o+"/script"+a),t.close(),u=t.F;while(r--)delete u[s][i[r]];return u()};t.exports=Object.create||function(t,e){var n;return null!==t?(c[s]=r(t),n=new c,c[s]=null,n[a]=t):n=u(),void 0===e?n:o(n,e)}},"2b4c":function(t,e,n){var r=n("5537")("wks"),o=n("ca5a"),i=n("7726").Symbol,a="function"==typeof i,c=t.exports=function(t){return r[t]||(r[t]=a&&i[t]||(a?i:o)("Symbol."+t))};c.store=r},"2d00":function(t,e){t.exports=!1},"2d1f":function(t,e,n){t.exports=n("42bb")},"2d95":function(t,e){var n={}.toString;t.exports=function(t){return n.call(t).slice(8,-1)}},"2ea1":function(t,e,n){var r=n("6f8a");t.exports=function(t,e){if(!r(t))return t;var n,o;if(e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;if("function"==typeof(n=t.valueOf)&&!r(o=n.call(t)))return o;if(!e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;throw TypeError("Can't convert object to primitive value")}},"2f21":function(t,e,n){"use strict";var r=n("79e5");t.exports=function(t,e){return!!t&&r(function(){e?t.call(null,function(){},1):t.call(null)})}},"2f62":function(t,e,n){"use strict"; +/** + * vuex v3.1.0 + * (c) 2019 Evan You + * @license MIT + */function r(t){var e=Number(t.version.split(".")[0]);if(e>=2)t.mixin({beforeCreate:r});else{var n=t.prototype._init;t.prototype._init=function(t){void 0===t&&(t={}),t.init=t.init?[r].concat(t.init):r,n.call(this,t)}}function r(){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)}}var o="undefined"!==typeof window&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__;function i(t){o&&(t._devtoolHook=o,o.emit("vuex:init",t),o.on("vuex:travel-to-state",function(e){t.replaceState(e)}),t.subscribe(function(t,e){o.emit("vuex:mutation",t,e)}))}function a(t,e){Object.keys(t).forEach(function(n){return e(t[n],n)})}function c(t){return null!==t&&"object"===typeof t}function s(t){return t&&"function"===typeof t.then}var u=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)||{}},f={namespaced:{configurable:!0}};f.namespaced.get=function(){return!!this._rawModule.namespaced},u.prototype.addChild=function(t,e){this._children[t]=e},u.prototype.removeChild=function(t){delete this._children[t]},u.prototype.getChild=function(t){return this._children[t]},u.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)},u.prototype.forEachChild=function(t){a(this._children,t)},u.prototype.forEachGetter=function(t){this._rawModule.getters&&a(this._rawModule.getters,t)},u.prototype.forEachAction=function(t){this._rawModule.actions&&a(this._rawModule.actions,t)},u.prototype.forEachMutation=function(t){this._rawModule.mutations&&a(this._rawModule.mutations,t)},Object.defineProperties(u.prototype,f);var l=function(t){this.register([],t,!1)};function p(t,e,n){if(e.update(n),n.modules)for(var r in n.modules){if(!e.getChild(r))return void 0;p(t.concat(r),e.getChild(r),n.modules[r])}}l.prototype.get=function(t){return t.reduce(function(t,e){return t.getChild(e)},this.root)},l.prototype.getNamespace=function(t){var e=this.root;return t.reduce(function(t,n){return e=e.getChild(n),t+(e.namespaced?n+"/":"")},"")},l.prototype.update=function(t){p([],this.root,t)},l.prototype.register=function(t,e,n){var r=this;void 0===n&&(n=!0);var o=new u(e,n);if(0===t.length)this.root=o;else{var i=this.get(t.slice(0,-1));i.addChild(t[t.length-1],o)}e.modules&&a(e.modules,function(e,o){r.register(t.concat(o),e,n)})},l.prototype.unregister=function(t){var e=this.get(t.slice(0,-1)),n=t[t.length-1];e.getChild(n).runtime&&e.removeChild(n)};var d;var h=function(t){var e=this;void 0===t&&(t={}),!d&&"undefined"!==typeof window&&window.Vue&&k(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 l(t),this._modulesNamespaceMap=Object.create(null),this._subscribers=[],this._watcherVM=new d;var o=this,a=this,c=a.dispatch,s=a.commit;this.dispatch=function(t,e){return c.call(o,t,e)},this.commit=function(t,e,n){return s.call(o,t,e,n)},this.strict=r;var u=this._modules.root.state;b(this,u,[],this._modules.root),m(this,u),n.forEach(function(t){return t(e)});var f=void 0!==t.devtools?t.devtools:d.config.devtools;f&&i(this)},v={state:{configurable:!0}};function y(t,e){return e.indexOf(t)<0&&e.push(t),function(){var n=e.indexOf(t);n>-1&&e.splice(n,1)}}function g(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;b(t,n,[],t._modules.root,!0),m(t,n,e)}function m(t,e,n){var r=t._vm;t.getters={};var o=t._wrappedGetters,i={};a(o,function(e,n){i[n]=function(){return e(t)},Object.defineProperty(t.getters,n,{get:function(){return t._vm[n]},enumerable:!0})});var c=d.config.silent;d.config.silent=!0,t._vm=new d({data:{$$state:e},computed:i}),d.config.silent=c,t.strict&&$(t),r&&(n&&t._withCommit(function(){r._data.$$state=null}),d.nextTick(function(){return r.$destroy()}))}function b(t,e,n,r,o){var i=!n.length,a=t._modules.getNamespace(n);if(r.namespaced&&(t._modulesNamespaceMap[a]=r),!i&&!o){var c=A(e,n.slice(0,-1)),s=n[n.length-1];t._withCommit(function(){d.set(c,s,r.state)})}var u=r.context=_(t,a,n);r.forEachMutation(function(e,n){var r=a+n;x(t,r,e,u)}),r.forEachAction(function(e,n){var r=e.root?n:a+n,o=e.handler||e;O(t,r,o,u)}),r.forEachGetter(function(e,n){var r=a+n;S(t,r,e,u)}),r.forEachChild(function(r,i){b(t,e,n.concat(i),r,o)})}function _(t,e,n){var r=""===e,o={dispatch:r?t.dispatch:function(n,r,o){var i=C(n,r,o),a=i.payload,c=i.options,s=i.type;return c&&c.root||(s=e+s),t.dispatch(s,a)},commit:r?t.commit:function(n,r,o){var i=C(n,r,o),a=i.payload,c=i.options,s=i.type;c&&c.root||(s=e+s),t.commit(s,a,c)}};return Object.defineProperties(o,{getters:{get:r?function(){return t.getters}:function(){return w(t,e)}},state:{get:function(){return A(t.state,n)}}}),o}function w(t,e){var n={},r=e.length;return 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})}}),n}function x(t,e,n,r){var o=t._mutations[e]||(t._mutations[e]=[]);o.push(function(e){n.call(t,r.state,e)})}function O(t,e,n,r){var o=t._actions[e]||(t._actions[e]=[]);o.push(function(e,o){var i=n.call(t,{dispatch:r.dispatch,commit:r.commit,getters:r.getters,state:r.state,rootGetters:t.getters,rootState:t.state},e,o);return s(i)||(i=Promise.resolve(i)),t._devtoolHook?i.catch(function(e){throw t._devtoolHook.emit("vuex:error",e),e}):i})}function S(t,e,n,r){t._wrappedGetters[e]||(t._wrappedGetters[e]=function(t){return n(r.state,r.getters,t.state,t.getters)})}function $(t){t._vm.$watch(function(){return this._data.$$state},function(){0},{deep:!0,sync:!0})}function A(t,e){return e.length?e.reduce(function(t,e){return t[e]},t):t}function C(t,e,n){return c(t)&&t.type&&(n=e,e=t,t=t.type),{type:t,payload:e,options:n}}function k(t){d&&t===d||(d=t,r(d))}v.state.get=function(){return this._vm._data.$$state},v.state.set=function(t){0},h.prototype.commit=function(t,e,n){var r=this,o=C(t,e,n),i=o.type,a=o.payload,c=(o.options,{type:i,payload:a}),s=this._mutations[i];s&&(this._withCommit(function(){s.forEach(function(t){t(a)})}),this._subscribers.forEach(function(t){return t(c,r.state)}))},h.prototype.dispatch=function(t,e){var n=this,r=C(t,e),o=r.type,i=r.payload,a={type:o,payload:i},c=this._actions[o];if(c){try{this._actionSubscribers.filter(function(t){return t.before}).forEach(function(t){return t.before(a,n.state)})}catch(u){0}var s=c.length>1?Promise.all(c.map(function(t){return t(i)})):c[0](i);return s.then(function(t){try{n._actionSubscribers.filter(function(t){return t.after}).forEach(function(t){return t.after(a,n.state)})}catch(u){0}return t})}},h.prototype.subscribe=function(t){return y(t,this._subscribers)},h.prototype.subscribeAction=function(t){var e="function"===typeof t?{before:t}:t;return y(e,this._actionSubscribers)},h.prototype.watch=function(t,e,n){var r=this;return this._watcherVM.$watch(function(){return t(r.state,r.getters)},e,n)},h.prototype.replaceState=function(t){var e=this;this._withCommit(function(){e._vm._data.$$state=t})},h.prototype.registerModule=function(t,e,n){void 0===n&&(n={}),"string"===typeof t&&(t=[t]),this._modules.register(t,e),b(this,this.state,t,this._modules.get(t),n.preserveState),m(this,this.state)},h.prototype.unregisterModule=function(t){var e=this;"string"===typeof t&&(t=[t]),this._modules.unregister(t),this._withCommit(function(){var n=A(e.state,t.slice(0,-1));d.delete(n,t[t.length-1])}),g(this)},h.prototype.hotUpdate=function(t){this._modules.update(t),g(this,!0)},h.prototype._withCommit=function(t){var e=this._committing;this._committing=!0,t(),this._committing=e},Object.defineProperties(h.prototype,v);var E=L(function(t,e){var n={};return D(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=I(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}),M=L(function(t,e){var n={};return D(e).forEach(function(e){var r=e.key,o=e.val;n[r]=function(){var e=[],n=arguments.length;while(n--)e[n]=arguments[n];var r=this.$store.commit;if(t){var i=I(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}),j=L(function(t,e){var n={};return D(e).forEach(function(e){var r=e.key,o=e.val;o=t+o,n[r]=function(){if(!t||I(this.$store,"mapGetters",t))return this.$store.getters[o]},n[r].vuex=!0}),n}),T=L(function(t,e){var n={};return D(e).forEach(function(e){var r=e.key,o=e.val;n[r]=function(){var e=[],n=arguments.length;while(n--)e[n]=arguments[n];var r=this.$store.dispatch;if(t){var i=I(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}),P=function(t){return{mapState:E.bind(null,t),mapGetters:j.bind(null,t),mapMutations:M.bind(null,t),mapActions:T.bind(null,t)}};function D(t){return 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 L(t){return function(e,n){return"string"!==typeof e?(n=e,e=""):"/"!==e.charAt(e.length-1)&&(e+="/"),t(e,n)}}function I(t,e,n){var r=t._modulesNamespaceMap[n];return r}var N={Store:h,install:k,version:"3.1.0",mapState:E,mapMutations:M,mapGetters:j,mapActions:T,createNamespacedHelpers:P};e["a"]=N},"2fdb":function(t,e,n){"use strict";var r=n("5ca1"),o=n("d2c8"),i="includes";r(r.P+r.F*n("5147")(i),"String",{includes:function(t){return!!~o(this,t,i).indexOf(t,arguments.length>1?arguments[1]:void 0)}})},"302f":function(t,e,n){var r=n("0f89"),o=n("f2fe"),i=n("1b55")("species");t.exports=function(t,e){var n,a=r(t).constructor;return void 0===a||void 0==(n=r(a)[i])?e:o(n)}},"312a":function(t,e,n){var r=n("ca38"),o=n("b808"),i="__core-js_shared__",a=o[i]||(o[i]={});(t.exports=function(t,e){return a[t]||(a[t]=void 0!==e?e:{})})("versions",[]).push({version:r.version,mode:n("e6a1")?"pure":"global",copyright:"© 2019 Denis Pushkarev (zloirock.ru)"})},"31c2":function(t,e){e.f=Object.getOwnPropertySymbols},"32e9":function(t,e,n){var r=n("86cc"),o=n("4630");t.exports=n("9e1e")?function(t,e,n){return r.f(t,e,o(1,n))}:function(t,e,n){return t[e]=n,t}},3360:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=function(){for(var t=arguments.length,e=Array(t),n=0;n0&&e.reduce(function(e,n){return e&&n.apply(t,r)},!0)})}},3457:function(t,e,n){var r=n("6f8a"),o=Math.floor;t.exports=function(t){return!r(t)&&isFinite(t)&&o(t)===t}},"36dc":function(t,e,n){var r=n("da3c"),o=n("df0a").set,i=r.MutationObserver||r.WebKitMutationObserver,a=r.process,c=r.Promise,s="process"==n("6e1f")(a);t.exports=function(){var t,e,n,u=function(){var r,o;s&&(r=a.domain)&&r.exit();while(t){o=t.fn,t=t.next;try{o()}catch(i){throw t?n():e=void 0,i}}e=void 0,r&&r.enter()};if(s)n=function(){a.nextTick(u)};else if(!i||r.navigator&&r.navigator.standalone)if(c&&c.resolve){var f=c.resolve(void 0);n=function(){f.then(u)}}else n=function(){o.call(r,u)};else{var l=!0,p=document.createTextNode("");new i(u).observe(p,{characterData:!0}),n=function(){p.data=l=!l}}return function(r){var o={fn:r,next:void 0};e&&(e.next=o),t||(t=o,n()),e=o}}},3846:function(t,e,n){n("9e1e")&&"g"!=/./g.flags&&n("86cc").f(RegExp.prototype,"flags",{configurable:!0,get:n("0bfb")})},"384f":function(t,e,n){var r=n("d13f"),o=n("11ff");r(r.G+r.F*(parseFloat!=o),{parseFloat:o})},"386b":function(t,e,n){var r=n("5ca1"),o=n("79e5"),i=n("be13"),a=/"/g,c=function(t,e,n,r){var o=String(i(t)),c="<"+e;return""!==n&&(c+=" "+n+'="'+String(r).replace(a,""")+'"'),c+">"+o+""};t.exports=function(t,e){var n={};n[t]=e(c),r(r.P+r.F*o(function(){var e=""[t]('"');return e!==e.toLowerCase()||e.split('"').length>3}),"String",n)}},"386d":function(t,e,n){"use strict";var r=n("cb7c"),o=n("83a1"),i=n("5f1b");n("214f")("search",1,function(t,e,n,a){return[function(n){var r=t(this),o=void 0==n?void 0:n[e];return void 0!==o?o.call(n,r):new RegExp(n)[e](String(r))},function(t){var e=a(n,t,this);if(e.done)return e.value;var c=r(t),s=String(this),u=c.lastIndex;o(u,0)||(c.lastIndex=0);var f=i(c,s);return o(c.lastIndex,u)||(c.lastIndex=u),null===f?-1:f.index}]})},"38fd":function(t,e,n){var r=n("69a8"),o=n("4bf8"),i=n("613b")("IE_PROTO"),a=Object.prototype;t.exports=Object.getPrototypeOf||function(t){return t=o(t),r(t,i)?t[i]:"function"==typeof t.constructor&&t instanceof t.constructor?t.constructor.prototype:t instanceof Object?a:null}},3904:function(t,e,n){var r=n("8ce0");t.exports=function(t,e,n){for(var o in e)n&&t[o]?t[o]=e[o]:r(t,o,e[o]);return t}},"3a54":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=(0,r.regex)("alphaNum",/^[a-zA-Z0-9]*$/)},"3adc":function(t,e,n){var r=n("0f89"),o=n("a47f"),i=n("2ea1"),a=Object.defineProperty;e.f=n("7d95")?Object.defineProperty:function(t,e,n){if(r(t),e=i(e,!0),r(n),o)try{return a(t,e,n)}catch(c){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(t[e]=n.value),t}},"3b2b":function(t,e,n){var r=n("7726"),o=n("5dbc"),i=n("86cc").f,a=n("9093").f,c=n("aae3"),s=n("0bfb"),u=r.RegExp,f=u,l=u.prototype,p=/a/g,d=/a/g,h=new u(p)!==p;if(n("9e1e")&&(!h||n("79e5")(function(){return d[n("2b4c")("match")]=!1,u(p)!=p||u(d)==d||"/a/i"!=u(p,"i")}))){u=function(t,e){var n=this instanceof u,r=c(t),i=void 0===e;return!n&&r&&t.constructor===u&&i?t:o(h?new f(r&&!i?t.source:t,e):f((r=t instanceof u)?t.source:t,r&&i?s.call(t):e),n?this:l,u)};for(var v=function(t){t in u||i(u,t,{configurable:!0,get:function(){return f[t]},set:function(e){f[t]=e}})},y=a(f),g=0;y.length>g;)v(y[g++]);l.constructor=u,u.prototype=l,n("2aba")(r,"RegExp",u)}n("7a56")("RegExp")},"3bb1":function(t,e,n){var r=n("b67f")("unscopables"),o=Array.prototype;void 0==o[r]&&n("a0a8")(o,r,{}),t.exports=function(t){o[r][t]=!0}},"3be2":function(t,e,n){t.exports=n("71e1")},4052:function(t,e,n){var r=n("4a89"),o=Math.max,i=Math.min;t.exports=function(t,e){return t=r(t),t<0?o(t+e,0):i(t,e)}},"41a0":function(t,e,n){"use strict";var r=n("2aeb"),o=n("4630"),i=n("7f20"),a={};n("32e9")(a,n("2b4c")("iterator"),function(){return this}),t.exports=function(t,e,n){t.prototype=r(a,{next:o(1,n)}),i(t,e+" Iterator")}},"42bb":function(t,e,n){n("fd6f"),t.exports=n("a7d3").Object.entries},"436c":function(t,e,n){var r=n("1b55")("iterator"),o=!1;try{var i=[7][r]();i["return"]=function(){o=!0},Array.from(i,function(){throw 2})}catch(a){}t.exports=function(t,e){if(!e&&!o)return!1;var n=!1;try{var i=[7],c=i[r]();c.next=function(){return{done:n=!0}},i[r]=function(){return c},t(i)}catch(a){}return n}},"43c8":function(t,e){var n={}.hasOwnProperty;t.exports=function(t,e){return n.call(t,e)}},4588:function(t,e){var n=Math.ceil,r=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?r:n)(t)}},"45b8":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=(0,r.regex)("numeric",/^[0-9]*$/)},"45e2":function(t,e,n){t.exports=!n("b629")(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},4630:function(t,e){t.exports=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}}},"46bc":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=function(t){return(0,r.withParams)({type:"maxValue",max:t},function(e){return!(0,r.req)(e)||(!/\s/.test(e)||e instanceof Date)&&+e<=+t})}},4842:function(t,e,n){var r=n("569f");r(r.S+r.F,"Object",{assign:n("a402")})},4917:function(t,e,n){"use strict";var r=n("cb7c"),o=n("9def"),i=n("0390"),a=n("5f1b");n("214f")("match",1,function(t,e,n,c){return[function(n){var r=t(this),o=void 0==n?void 0:n[e];return void 0!==o?o.call(n,r):new RegExp(n)[e](String(r))},function(t){var e=c(n,t,this);if(e.done)return e.value;var s=r(t),u=String(this);if(!s.global)return a(s,u);var f=s.unicode;s.lastIndex=0;var l,p=[],d=0;while(null!==(l=a(s,u))){var h=String(l[0]);p[d]=h,""===h&&(s.lastIndex=i(u,o(s.lastIndex),f)),d++}return 0===d?null:p}]})},4938:function(t,e,n){var r=n("6a9b"),o=n("626e").f;n("c165")("getOwnPropertyDescriptor",function(){return function(t,e){return o(r(t),e)}})},"49c1":function(t,e){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},"4a89":function(t,e){var n=Math.ceil,r=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?r:n)(t)}},"4b9e":function(t,e,n){var r=n("b808"),o=r.navigator;t.exports=o&&o.userAgent||""},"4bf8":function(t,e,n){var r=n("be13");t.exports=function(t){return Object(r(t))}},"4cf4":function(t,e,n){var r=n("0244");t.exports=function(t){return Object(r(t))}},5147:function(t,e,n){var r=n("2b4c")("match");t.exports=function(t){var e=/./;try{"/./"[t](e)}catch(n){try{return e[r]=!1,!"/./"[t](e)}catch(o){}}return!0}},5176:function(t,e,n){t.exports=n("81ee")},"520a":function(t,e,n){"use strict";var r=n("0bfb"),o=RegExp.prototype.exec,i=String.prototype.replace,a=o,c="lastIndex",s=function(){var t=/a/,e=/b*/g;return o.call(t,"a"),o.call(e,"a"),0!==t[c]||0!==e[c]}(),u=void 0!==/()??/.exec("")[1],f=s||u;f&&(a=function(t){var e,n,a,f,l=this;return u&&(n=new RegExp("^"+l.source+"$(?!\\s)",r.call(l))),s&&(e=l[c]),a=o.call(l,t),s&&a&&(l[c]=l.global?a.index+a[0].length:e),u&&a&&a.length>1&&i.call(a[0],n,function(){for(f=1;f + * @author owenm + * @license MIT + */ +/**! + * Sortable + * @author RubaXa + * @author owenm + * @license MIT + */ +(function(i){"use strict";r=i,o="function"===typeof r?r.call(e,n,e,t):r,void 0===o||(t.exports=o)})(function(){"use strict";if("undefined"===typeof window||!window.document)return function(){throw new Error("Sortable.js requires a window with a document")};var t,e,n,r,o,i,a,c,s,u,f,l,p,d,h,v,y,g,m,b,_,w,x,O,S,$,A=[],C=!1,k=!1,E=!1,M=[],j=!1,T=!1,P=[],D=/\s+/g,L="Sortable"+(new Date).getTime(),I=window,N=I.document,R=I.parseInt,F=I.setTimeout,H=I.jQuery||I.Zepto,z=I.Polymer,V={capture:!1,passive:!1},B=!!navigator.userAgent.match(/(?:Trident.*rv[ :]?11\.|msie|iemobile)/i),U=!!navigator.userAgent.match(/Edge/i),Y=!!navigator.userAgent.match(/firefox/i),q=!(!navigator.userAgent.match(/safari/i)||navigator.userAgent.match(/chrome/i)||navigator.userAgent.match(/android/i)),W=!!navigator.userAgent.match(/iP(ad|od|hone)/i),G=W,K=U||B?"cssFloat":"float",X="draggable"in N.createElement("div"),J=function(){if(B)return!1;var t=N.createElement("x");return t.style.cssText="pointer-events:auto","auto"===t.style.pointerEvents}(),Z=!1,Q=!1,tt=Math.abs,et=Math.min,nt=Math.max,rt=[],ot=function(t,e){var n=kt(t),r=R(n.width)-R(n.paddingLeft)-R(n.paddingRight)-R(n.borderLeftWidth)-R(n.borderRightWidth),o=Lt(t,0,e),i=Lt(t,1,e),a=o&&kt(o),c=i&&kt(i),s=a&&R(a.marginLeft)+R(a.marginRight)+Xt(o).width,u=c&&R(c.marginLeft)+R(c.marginRight)+Xt(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&&"none"!==a.float){var f="left"===a.float?"left":"right";return!i||"both"!==c.clear&&c.clear!==f?"horizontal":"vertical"}return o&&("block"===a.display||"flex"===a.display||"table"===a.display||"grid"===a.display||s>=r&&"none"===n[K]||i&&"none"===n[K]&&s+u>r)?"vertical":"horizontal"},it=function(t,e){for(var n=0;n=r.left-o&&t<=r.right+o,a=e>=r.top-o&&e<=r.bottom+o;if(i&&a)return M[n]}},at=function(t,e,n,r,o){var i=Xt(n),a="vertical"===r?i.left:i.top,c="vertical"===r?i.right:i.bottom,s="vertical"===r?t:e;return a-1}}var n={},r=t.group;r&&"object"==typeof 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},ht=function(e){t&&t.parentNode&&t.parentNode[L]&&t.parentNode[L]._computeIsAligned(e)},vt=function(t,e){var n=e;while(!n[L])n=n.parentNode;return t===n},yt=function(t,e,n){var r=t.parentNode;while(r&&!r[L])r=r.parentNode;r&&r[L][n](Yt(e,{artificialBubble:!0}))},gt=function(){!J&&n&&kt(n,"display","none")},mt=function(){!J&&n&&kt(n,"display","")};N.addEventListener("click",function(t){if(E)return t.preventDefault(),t.stopPropagation&&t.stopPropagation(),t.stopImmediatePropagation&&t.stopImmediatePropagation(),E=!1,!1},!0);var bt,_t=function(e){if(e=e.touches?e.touches[0]:e,t){var n=it(e.clientX,e.clientY);n&&n[L]._onDragOver({clientX:e.clientX,clientY:e.clientY,target:n,rootEl:n})}};function wt(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be HTMLElement, not "+{}.toString.call(t);this.el=t,this.options=e=Yt({},e),t[L]=this;var n={group:null,sort:!0,disabled:!1,store:null,handle:null,scroll:!0,scrollSensitivity:30,scrollSpeed:10,bubbleScroll:!0,draggable:/[uo]l/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return ot(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,touchStartThreshold:R(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==wt.supportPointer&&("PointerEvent"in window||window.navigator&&"msPointerEnabled"in window.navigator),emptyInsertThreshold:5};for(var r in n)!(r in e)&&(e[r]=n[r]);for(var o in dt(e),this)"_"===o.charAt(0)&&"function"===typeof this[o]&&(this[o]=this[o].bind(this));this.nativeDraggable=!e.forceFallback&&X,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?$t(t,"pointerdown",this._onTapStart):($t(t,"mousedown",this._onTapStart),$t(t,"touchstart",this._onTapStart)),this.nativeDraggable&&($t(t,"dragover",this),$t(t,"dragenter",this)),M.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[])}function xt(t,e,n,r){if(t){n=n||N;do{if(null!=e&&(">"===e[0]&&t.parentNode===n&&Vt(t,e.substring(1))||Vt(t,e))||r&&t===n)return t;if(t===n)break}while(t=Ot(t))}return null}function Ot(t){return t.host&&t!==N&&t.host.nodeType?t.host:t.parentNode}function St(t){t.dataTransfer&&(t.dataTransfer.dropEffect="move"),t.cancelable&&t.preventDefault()}function $t(t,e,n){t.addEventListener(e,n,V)}function At(t,e,n){t.removeEventListener(e,n,V)}function Ct(t,e,n){if(t&&e)if(t.classList)t.classList[n?"add":"remove"](e);else{var r=(" "+t.className+" ").replace(D," ").replace(" "+e+" "," ");t.className=(r+(n?" "+e:"")).replace(D," ")}}function kt(t,e,n){var r=t&&t.style;if(r){if(void 0===n)return N.defaultView&&N.defaultView.getComputedStyle?n=N.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 Et(t){var e="";do{var n=kt(t,"transform");n&&"none"!==n&&(e=n+" "+e)}while(t=t.parentNode);return window.DOMMatrix?new DOMMatrix(e):window.WebKitCSSMatrix?new WebKitCSSMatrix(e):window.CSSMatrix?new CSSMatrix(e):void 0}function Mt(t,e,n){if(t){var r=t.getElementsByTagName(e),o=0,i=r.length;if(n)for(;os+u||i<=s&&o>a&&i>=c:o>a&&i>c||o<=a&&i>s+u}function Rt(e,n,r,o,i,a,c){var s=Xt(n),u="vertical"===r?e.clientY:e.clientX,f="vertical"===r?s.height:s.width,l="vertical"===r?s.top:s.left,p="vertical"===r?s.bottom:s.right,d=Xt(t),h=!1;if(!a)if(c&&Ol+f*i/2:up-O)return-1*w}else if(u>l+f*(1-o)/2&&up-f*i/2)?u>l+f/2?1:-1:0}function Ft(e){var n=zt(t),r=zt(e);return n=i:r<=i,!o)return n;if(n===ut())break;n=st(n,!1)}return!1}function Zt(t){var e=0,n=0,r=ut();if(t)do{var o=Et(t),i=o.a,a=o.d;e+=t.scrollLeft*i,n+=t.scrollTop*a}while(t!==r&&(t=t.parentNode));return[e,n]}return $t(N,"dragover",_t),$t(N,"mousemove",_t),$t(N,"touchmove",_t),wt.prototype={constructor:wt,_computeIsAligned:function(e){var r;if(n&&!J?(gt(),r=N.elementFromPoint(e.clientX,e.clientY),mt()):r=e.target,r=xt(r,this.options.draggable,this.el,!1),!Q&&t&&t.parentNode===this.el){for(var o=this.el.children,i=0;i=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){t&&Pt(t),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;At(t,"mouseup",this._disableDelayedDrag),At(t,"touchend",this._disableDelayedDrag),At(t,"touchcancel",this._disableDelayedDrag),At(t,"mousemove",this._delayedDragTouchMoveHandler),At(t,"touchmove",this._delayedDragTouchMoveHandler),At(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(e,n){n=n||("touch"==e.pointerType?e:null),!this.nativeDraggable||n?this.options.supportPointer?$t(N,"pointermove",this._onTouchMove):$t(N,n?"touchmove":"mousemove",this._onTouchMove):($t(t,"dragend",this),$t(o,"dragstart",this._onDragStart));try{N.selection?Gt(function(){N.selection.empty()}):window.getSelection().removeAllRanges()}catch(r){}},_dragStarted:function(e,n){if(k=!1,o&&t){this.nativeDraggable&&($t(N,"dragover",this._handleAutoScroll),$t(N,"dragover",ht));var r=this.options;!e&&Ct(t,r.dragClass,!1),Ct(t,r.ghostClass,!0),kt(t,"transform",""),wt.active=this,e&&this._appendGhost(),jt(this,o,"start",t,o,o,f,void 0,n)}else this._nulling()},_emulateDragOver:function(e){if(m){if(this._lastX===m.clientX&&this._lastY===m.clientY&&!e)return;this._lastX=m.clientX,this._lastY=m.clientY,gt();var n=N.elementFromPoint(m.clientX,m.clientY),r=n;while(n&&n.shadowRoot)n=n.shadowRoot.elementFromPoint(m.clientX,m.clientY),r=n;if(r)do{var o;if(r[L])if(o=r[L]._onDragOver({clientX:m.clientX,clientY:m.clientY,target:n,rootEl:r}),o&&!this.options.dragoverBubble)break;n=r}while(r=r.parentNode);t.parentNode[L]._computeIsAligned(m),mt()}},_onTouchMove:function(t,e){if(g){var r=this.options,o=r.fallbackTolerance,i=r.fallbackOffset,a=t.touches?t.touches[0]:t,c=n&&Et(n),s=n&&c&&c.a,u=n&&c&&c.d,f=G&&S&&Zt(S),l=(a.clientX-g.clientX+i.x)/(s||1)+(f?f[0]-P[0]:0)/(s||1),p=(a.clientY-g.clientY+i.y)/(u||1)+(f?f[1]-P[1]:0)/(u||1),d=t.touches?"translate3d("+l+"px,"+p+"px,0)":"translate("+l+"px,"+p+"px)";if(!wt.active&&!k){if(o&&et(tt(a.clientX-this._lastX),tt(a.clientY-this._lastY))=0&&(jt(null,e,"add",t,e,o,f,l,a),jt(this,o,"remove",t,e,o,f,l,a),jt(null,e,"sort",t,e,o,f,l,a),jt(this,o,"sort",t,e,o,f,l,a)),d&&d.save()):t.nextSibling!==i&&(l=zt(t,s.draggable),l>=0&&(jt(this,o,"update",t,e,o,f,l,a),jt(this,o,"sort",t,e,o,f,l,a))),wt.active&&(null!=l&&-1!==l||(l=f),jt(this,o,"end",t,e,o,f,l,a),this.save()))),this._nulling()},_nulling:function(){o=t=e=n=i=r=a=c=s=A.length=h=v=y=g=m=b=l=f=_=w=$=d=p=wt.active=null,rt.forEach(function(t){t.checked=!0}),rt.length=0},handleEvent:function(e){switch(e.type){case"drop":case"dragend":this._onDrop(e);break;case"dragenter":case"dragover":t&&(this._onDragOver(e),St(e));break;case"selectstart":e.preventDefault();break}},toArray:function(){for(var t,e=[],n=this.el.children,r=0,o=n.length,i=this.options;rb;b++)if(y=e?m(a(h=t[b])[0],h[1]):m(t[b]),y===u||y===f)return y}else for(v=g.call(t);!(h=v.next()).done;)if(y=o(v,m,h.value,e),y===u||y===f)return y};e.BREAK=u,e.RETURN=f},"565d":function(t,e,n){var r=n("6a9b"),o=n("d876").f,i={}.toString,a="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[],c=function(t){try{return o(t)}catch(e){return a.slice()}};t.exports.f=function(t){return a&&"[object Window]"==i.call(t)?c(t):o(r(t))}},5698:function(t,e,n){n("d256"),t.exports=n("a7d3").Object.getOwnPropertySymbols},"569f":function(t,e,n){var r=n("b808"),o=n("ca38"),i=n("a0a8"),a=n("1f51"),c=n("a9f2"),s="prototype",u=function(t,e,n){var f,l,p,d,h=t&u.F,v=t&u.G,y=t&u.S,g=t&u.P,m=t&u.B,b=v?r:y?r[e]||(r[e]={}):(r[e]||{})[s],_=v?o:o[e]||(o[e]={}),w=_[s]||(_[s]={});for(f in v&&(n=e),n)l=!h&&b&&void 0!==b[f],p=(l?b:n)[f],d=m&&l?c(p,r):g&&"function"==typeof p?c(Function.call,p):p,b&&a(b,f,p,t&u.U),_[f]!=p&&i(_,f,d),g&&w[f]!=p&&(w[f]=p)};r.core=o,u.F=1,u.G=2,u.S=4,u.P=8,u.B=16,u.W=32,u.U=64,u.R=128,t.exports=u},"57f7":function(t,e,n){n("93c4"),n("6109"),t.exports=n("a7d3").Array.from},"58b9":function(t,e,n){var r=n("d43f"),o=n("0244");t.exports=function(t){return r(o(t))}},5927:function(t,e,n){n("93c4"),n("b42c"),t.exports=n("fda1").f("iterator")},"59ad":function(t,e,n){t.exports=n("0965")},"5a0c":function(t,e,n){!function(e,n){t.exports=n()}(0,function(){"use strict";var t="millisecond",e="second",n="minute",r="hour",o="day",i="week",a="month",c="quarter",s="year",u=/^(\d{4})-?(\d{1,2})-?(\d{0,2})[^0-9]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?.?(\d{1,3})?$/,f=/\[([^\]]+)]|Y{2,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,l=function(t,e,n){var r=String(t);return!r||r.length>=e?t:""+Array(e+1-r.length).join(n)+t},p={s:l,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),o=n%60;return(e<=0?"+":"-")+l(r,2,"0")+":"+l(o,2,"0")},m:function(t,e){var n=12*(e.year()-t.year())+(e.month()-t.month()),r=t.clone().add(n,a),o=e-r<0,i=t.clone().add(n+(o?-1:1),a);return Number(-(n+(e-r)/(o?r-i:i-r))||0)},a:function(t){return t<0?Math.ceil(t)||0:Math.floor(t)},p:function(u){return{M:a,y:s,w:i,d:o,h:r,m:n,s:e,ms:t,Q:c}[u]||String(u||"").toLowerCase().replace(/s$/,"")},u:function(t){return void 0===t}},d={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_")},h="en",v={};v[h]=d;var y=function(t){return t instanceof _},g=function(t,e,n){var r;if(!t)return null;if("string"==typeof t)v[t]&&(r=t),e&&(v[t]=e,r=t);else{var o=t.name;v[o]=t,r=o}return n||(h=r),r},m=function(t,e,n){if(y(t))return t.clone();var r=e?"string"==typeof e?{format:e,pl:n}:e:{};return r.date=t,new _(r)},b=p;b.l=g,b.i=y,b.w=function(t,e){return m(t,{locale:e.$L,utc:e.$u})};var _=function(){function l(t){this.$L=this.$L||g(t.locale,null,!0)||h,this.parse(t)}var p=l.prototype;return p.parse=function(t){this.$d=function(t){var e=t.date,n=t.utc;if(null===e)return new Date(NaN);if(b.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(u);if(r)return n?new Date(Date.UTC(r[1],r[2]-1,r[3]||1,r[4]||0,r[5]||0,r[6]||0,r[7]||0)):new Date(r[1],r[2]-1,r[3]||1,r[4]||0,r[5]||0,r[6]||0,r[7]||0)}return new Date(e)}(t),this.init()},p.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()},p.$utils=function(){return b},p.isValid=function(){return!("Invalid Date"===this.$d.toString())},p.isSame=function(t,e){var n=m(t);return this.startOf(e)<=n&&n<=this.endOf(e)},p.isAfter=function(t,e){return m(t)i)a(n[i++]);t._c=[],t._n=!1,e&&!t._h&&L(t)})}},L=function(t){g.call(s,function(){var e,n,r,o=t._v,i=I(t);if(i&&(e=_(function(){E?$.emit("unhandledRejection",o,t):(n=s.onunhandledrejection)?n({promise:t,reason:o}):(r=s.console)&&r.error&&r.error("Unhandled promise rejection",o)}),t._h=E||I(t)?2:1),t._a=void 0,i&&e.e)throw e.v})},I=function(t){return 1!==t._h&&0===(t._a||t._c).length},N=function(t){g.call(s,function(){var e;E?$.emit("rejectionHandled",t):(e=s.onrejectionhandled)&&e({promise:t,reason:t._v})})},R=function(t){var e=this;e._d||(e._d=!0,e=e._w||e,e._v=t,e._s=2,e._a||(e._a=e._c.slice()),D(e,!0))},F=function(t){var e,n=this;if(!n._d){n._d=!0,n=n._w||n;try{if(n===t)throw S("Promise can't be resolved itself");(e=P(t))?m(function(){var r={_w:n,_d:!1};try{e.call(t,u(F,r,1),u(R,r,1))}catch(o){R.call(r,o)}}):(n._v=t,n._s=1,D(n,!1))}catch(r){R.call({_w:n,_d:!1},r)}}};T||(k=function(t){h(this,k,O,"_h"),d(t),r.call(this);try{t(u(F,this,1),u(R,this,1))}catch(e){R.call(this,e)}},r=function(t){this._c=[],this._a=void 0,this._s=0,this._d=!1,this._v=void 0,this._h=0,this._n=!1},r.prototype=n("3904")(k.prototype,{then:function(t,e){var n=j(y(this,k));return n.ok="function"!=typeof t||t,n.fail="function"==typeof e&&e,n.domain=E?$.domain:void 0,this._c.push(n),this._a&&this._a.push(n),this._s&&D(this,!1),n.promise},catch:function(t){return this.then(void 0,t)}}),i=function(){var t=new r;this.promise=t,this.resolve=u(F,t,1),this.reject=u(R,t,1)},b.f=j=function(t){return t===k||t===a?new i(t):o(t)}),l(l.G+l.W+l.F*!T,{Promise:k}),n("c0d8")(k,O),n("1be4")(O),a=n("a7d3")[O],l(l.S+l.F*!T,O,{reject:function(t){var e=j(this),n=e.reject;return n(t),e.promise}}),l(l.S+l.F*(c||!T),O,{resolve:function(t){return x(c&&this===a?k:this,t)}}),l(l.S+l.F*!(T&&n("436c")(function(t){k.all(t)["catch"](M)})),O,{all:function(t){var e=this,n=j(e),r=n.resolve,o=n.reject,i=_(function(){var n=[],i=0,a=1;v(t,!1,function(t){var c=i++,s=!1;n.push(void 0),a++,e.resolve(t).then(function(t){s||(s=!0,n[c]=t,--a||r(n))},o)}),--a||r(n)});return i.e&&o(i.v),n.promise},race:function(t){var e=this,n=j(e),r=n.reject,o=_(function(){v(t,!1,function(t){e.resolve(t).then(n.resolve,r)})});return o.e&&r(o.v),n.promise}})},"5ca1":function(t,e,n){var r=n("7726"),o=n("8378"),i=n("32e9"),a=n("2aba"),c=n("9b43"),s="prototype",u=function(t,e,n){var f,l,p,d,h=t&u.F,v=t&u.G,y=t&u.S,g=t&u.P,m=t&u.B,b=v?r:y?r[e]||(r[e]={}):(r[e]||{})[s],_=v?o:o[e]||(o[e]={}),w=_[s]||(_[s]={});for(f in v&&(n=e),n)l=!h&&b&&void 0!==b[f],p=(l?b:n)[f],d=m&&l?c(p,r):g&&"function"==typeof p?c(Function.call,p):p,b&&a(b,f,p,t&u.U),_[f]!=p&&i(_,f,d),g&&w[f]!=p&&(w[f]=p)};r.core=o,u.F=1,u.G=2,u.S=4,u.P=8,u.B=16,u.W=32,u.U=64,u.R=128,t.exports=u},"5ce7":function(t,e,n){"use strict";var r=n("7108"),o=n("f845"),i=n("c0d8"),a={};n("8ce0")(a,n("1b55")("iterator"),function(){return this}),t.exports=function(t,e,n){t.prototype=r(a,{next:o(1,n)}),i(t,e+" Iterator")}},"5d58":function(t,e,n){t.exports=n("5927")},"5d73":function(t,e,n){t.exports=n("0a91")},"5d75":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef"),o=/(^$|^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$)/;e.default=(0,r.regex)("email",o)},"5d8f":function(t,e,n){var r=n("7772")("keys"),o=n("7b00");t.exports=function(t){return r[t]||(r[t]=o(t))}},"5db3":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=function(t){return(0,r.withParams)({type:"minLength",min:t},function(e){return!(0,r.req)(e)||(0,r.len)(e)>=t})}},"5dbc":function(t,e,n){var r=n("d3f4"),o=n("8b97").set;t.exports=function(t,e,n){var i,a=e.constructor;return a!==n&&"function"==typeof a&&(i=a.prototype)!==n.prototype&&r(i)&&o&&o(t,i),t}},"5df3":function(t,e,n){"use strict";var r=n("02f4")(!0);n("01f9")(String,"String",function(t){this._t=String(t),this._i=0},function(){var t,e=this._t,n=this._i;return n>=e.length?{value:void 0,done:!0}:(t=r(e,n),this._i+=t.length,{value:t,done:!1})})},"5f1b":function(t,e,n){"use strict";var r=n("23c6"),o=RegExp.prototype.exec;t.exports=function(t,e){var n=t.exec;if("function"===typeof n){var i=n.call(t,e);if("object"!==typeof i)throw new TypeError("RegExp exec method returned something other than an Object or null");return i}if("RegExp"!==r(t))throw new TypeError("RegExp#exec called on incompatible receiver");return o.call(t,e)}},6109:function(t,e,n){"use strict";var r=n("bc25"),o=n("d13f"),i=n("0185"),a=n("9c93"),c=n("c227"),s=n("a5ab"),u=n("b3ec"),f=n("f159");o(o.S+o.F*!n("436c")(function(t){Array.from(t)}),"Array",{from:function(t){var e,n,o,l,p=i(t),d="function"==typeof this?this:Array,h=arguments.length,v=h>1?arguments[1]:void 0,y=void 0!==v,g=0,m=f(p);if(y&&(v=r(v,h>2?arguments[2]:void 0,2)),void 0==m||d==Array&&c(m))for(e=s(p.length),n=new d(e);e>g;g++)u(n,g,y?v(p[g],g):p[g]);else for(l=m.call(p),n=new d;!(o=l.next()).done;g++)u(n,g,y?a(l,v,[o.value,g],!0):o.value);return n.length=g,n}})},"613b":function(t,e,n){var r=n("5537")("keys"),o=n("ca5a");t.exports=function(t){return r[t]||(r[t]=o(t))}},6235:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=(0,r.regex)("alpha",/^[a-zA-Z]*$/)},"626a":function(t,e,n){var r=n("2d95");t.exports=Object("z").propertyIsEnumerable(0)?Object:function(t){return"String"==r(t)?t.split(""):Object(t)}},"626e":function(t,e,n){var r=n("d74e"),o=n("f845"),i=n("6a9b"),a=n("2ea1"),c=n("43c8"),s=n("a47f"),u=Object.getOwnPropertyDescriptor;e.f=n("7d95")?u:function(t,e){if(t=i(t),e=a(e,!0),s)try{return u(t,e)}catch(n){}if(c(t,e))return o(!r.f.call(t,e),t[e])}},6277:function(t,e,n){var r=n("7b00")("meta"),o=n("6f8a"),i=n("43c8"),a=n("3adc").f,c=0,s=Object.isExtensible||function(){return!0},u=!n("d782")(function(){return s(Object.preventExtensions({}))}),f=function(t){a(t,r,{value:{i:"O"+ ++c,w:{}}})},l=function(t,e){if(!o(t))return"symbol"==typeof t?t:("string"==typeof t?"S":"P")+t;if(!i(t,r)){if(!s(t))return"F";if(!e)return"E";f(t)}return t[r].i},p=function(t,e){if(!i(t,r)){if(!s(t))return!0;if(!e)return!1;f(t)}return t[r].w},d=function(t){return u&&h.NEED&&s(t)&&!i(t,r)&&f(t),t},h=t.exports={KEY:r,NEED:!1,fastKey:l,getWeak:p,onFreeze:d}},"633a":function(t,e,n){var r=n("d13f"),o=n("e5fa"),i=n("d782"),a=n("702a"),c="["+a+"]",s="​…",u=RegExp("^"+c+c+"*"),f=RegExp(c+c+"*$"),l=function(t,e,n){var o={},c=i(function(){return!!a[t]()||s[t]()!=s}),u=o[t]=c?e(p):a[t];n&&(o[n]=u),r(r.P+r.F*c,"String",o)},p=l.trim=function(t,e){return t=String(o(t)),1&e&&(t=t.replace(u,"")),2&e&&(t=t.replace(f,"")),t};t.exports=l},6762:function(t,e,n){"use strict";var r=n("5ca1"),o=n("c366")(!0);r(r.P,"Array",{includes:function(t){return o(this,t,arguments.length>1?arguments[1]:void 0)}}),n("9c6c")("includes")},"67bb":function(t,e,n){t.exports=n("b258")},6821:function(t,e,n){var r=n("626a"),o=n("be13");t.exports=function(t){return r(o(t))}},"696b":function(t,e){e.f=Object.getOwnPropertySymbols},"69a8":function(t,e){var n={}.hasOwnProperty;t.exports=function(t,e){return n.call(t,e)}},"6a99":function(t,e,n){var r=n("d3f4");t.exports=function(t,e){if(!r(t))return t;var n,o;if(e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;if("function"==typeof(n=t.valueOf)&&!r(o=n.call(t)))return o;if(!e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;throw TypeError("Can't convert object to primitive value")}},"6a9b":function(t,e,n){var r=n("8bab"),o=n("e5fa");t.exports=function(t){return r(o(t))}},"6b54":function(t,e,n){"use strict";n("3846");var r=n("cb7c"),o=n("0bfb"),i=n("9e1e"),a="toString",c=/./[a],s=function(t){n("2aba")(RegExp.prototype,a,t,!0)};n("79e5")(function(){return"/a/b"!=c.call({source:"a",flags:"b"})})?s(function(){var t=r(this);return"/".concat(t.source,"/","flags"in t?t.flags:!i&&t instanceof RegExp?o.call(t):void 0)}):c.name!=a&&s(function(){return c.call(this)})},"6e1f":function(t,e){var n={}.toString;t.exports=function(t){return n.call(t).slice(8,-1)}},"6f8a":function(t,e){t.exports=function(t){return"object"===typeof t?null!==t:"function"===typeof t}},"702a":function(t,e){t.exports="\t\n\v\f\r   ᠎              \u2028\u2029\ufeff"},7108:function(t,e,n){var r=n("0f89"),o=n("f568"),i=n("0029"),a=n("5d8f")("IE_PROTO"),c=function(){},s="prototype",u=function(){var t,e=n("12fd")("iframe"),r=i.length,o="<",a=">";e.style.display="none",n("103a").appendChild(e),e.src="javascript:",t=e.contentWindow.document,t.open(),t.write(o+"script"+a+"document.F=Object"+o+"/script"+a),t.close(),u=t.F;while(r--)delete u[s][i[r]];return u()};t.exports=Object.create||function(t,e){var n;return null!==t?(c[s]=r(t),n=new c,c[s]=null,n[a]=t):n=u(),void 0===e?n:o(n,e)}},"71e1":function(t,e,n){n("a243"),t.exports=n("a7d3").Number.isInteger},"71fa":function(t,e){var n={}.toString;t.exports=function(t){return n.call(t).slice(8,-1)}},"73c3":function(t,e){t.exports=function(t){try{return{e:!1,v:t()}}catch(e){return{e:!0,v:e}}}},"75c9":function(t,e){t.exports=function(t){try{return{e:!1,v:t()}}catch(e){return{e:!0,v:e}}}},"75fc":function(t,e,n){"use strict";var r=n("a745"),o=n.n(r);function i(t){if(o()(t)){for(var e=0,n=new Array(t.length);e>>0||(a.test(n)?16:10))}:r},7726:function(t,e){var n=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},"772d":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef"),o=/^(?:(?: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-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:[\/?#]\S*)?$/i;e.default=(0,r.regex)("url",o)},"774e":function(t,e,n){t.exports=n("57f7")},7772:function(t,e,n){var r=n("a7d3"),o=n("da3c"),i="__core-js_shared__",a=o[i]||(o[i]={});(t.exports=function(t,e){return a[t]||(a[t]=void 0!==e?e:{})})("versions",[]).push({version:r.version,mode:n("b457")?"pure":"global",copyright:"© 2019 Denis Pushkarev (zloirock.ru)"})},"77f1":function(t,e,n){var r=n("4588"),o=Math.max,i=Math.min;t.exports=function(t,e){return t=r(t),t<0?o(t+e,0):i(t,e)}},"781f":function(t,e,n){var r=n("ab4c"),o=n("b808").document,i=r(o)&&r(o.createElement);t.exports=function(t){return i?o.createElement(t):{}}},"78ef":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.regex=e.ref=e.len=e.req=e.withParams=void 0;var r=n("8750"),o=i(r);function i(t){return t&&t.__esModule?t:{default:t}}e.withParams=o.default;var a=e.req=function(t){if(Array.isArray(t))return!!t.length;if(void 0===t||null===t||!1===t)return!1;if(t instanceof Date)return!isNaN(t.getTime());if("object"===typeof t){for(var e in t)return!0;return!1}return!!String(t).length};e.len=function(t){return Array.isArray(t)?t.length:"object"===typeof t?Object.keys(t).length:String(t).length},e.ref=function(t,e,n){return"function"===typeof t?t.call(e,n):n[t]},e.regex=function(t,e){return(0,o.default)({type:t},function(t){return!a(t)||e.test(t)})}},"795b":function(t,e,n){t.exports=n("dd04")},"79e5":function(t,e){t.exports=function(t){try{return!!t()}catch(e){return!0}}},"7a56":function(t,e,n){"use strict";var r=n("7726"),o=n("86cc"),i=n("9e1e"),a=n("2b4c")("species");t.exports=function(t){var e=r[t];i&&e&&!e[a]&&o.f(e,a,{configurable:!0,get:function(){return this}})}},"7b00":function(t,e){var n=0,r=Math.random();t.exports=function(t){return"Symbol(".concat(void 0===t?"":t,")_",(++n+r).toString(36))}},"7d8a":function(t,e,n){var r=n("6e1f"),o=n("1b55")("toStringTag"),i="Arguments"==r(function(){return arguments}()),a=function(t,e){try{return t[e]}catch(n){}};t.exports=function(t){var e,n,c;return void 0===t?"Undefined":null===t?"Null":"string"==typeof(n=a(e=Object(t),o))?n:i?r(e):"Object"==(c=r(e))&&"function"==typeof e.callee?"Arguments":c}},"7d95":function(t,e,n){t.exports=!n("d782")(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},"7f20":function(t,e,n){var r=n("86cc").f,o=n("69a8"),i=n("2b4c")("toStringTag");t.exports=function(t,e,n){t&&!o(t=n?t:t.prototype,i)&&r(t,i,{configurable:!0,value:e})}},"7f7f":function(t,e,n){var r=n("86cc").f,o=Function.prototype,i=/^\s*function ([^ (]*)/,a="name";a in o||n("9e1e")&&r(o,a,{configurable:!0,get:function(){try{return(""+this).match(i)[1]}catch(t){return""}}})},8115:function(t,e){t.exports=function(t,e,n,r){if(!(t instanceof e)||void 0!==r&&r in t)throw TypeError(n+": incorrect invocation!");return t}},"81ee":function(t,e,n){n("9a51"),t.exports=n("a7d3").Object.assign},8378:function(t,e){var n=t.exports={version:"2.6.0"};"number"==typeof __e&&(__e=n)},"83a1":function(t,e){t.exports=Object.is||function(t,e){return t===e?0!==t||1/t===1/e:t!=t&&e!=e}},"84f2":function(t,e){t.exports={}},"85f2":function(t,e,n){t.exports=n("ec5b")},"86cc":function(t,e,n){var r=n("cb7c"),o=n("c69a"),i=n("6a99"),a=Object.defineProperty;e.f=n("9e1e")?Object.defineProperty:function(t,e,n){if(r(t),e=i(e,!0),r(n),o)try{return a(t,e,n)}catch(c){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(t[e]=n.value),t}},8750:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("0234").withParams;e.default=r},"88b8":function(t,e,n){var r=n("a9f2"),o=n("1d27"),i=n("f26d"),a=n("27b2"),c=n("04cf"),s=n("b36f"),u={},f={};e=t.exports=function(t,e,n,l,p){var d,h,v,y,g=p?function(){return t}:s(t),m=r(n,l,e?2:1),b=0;if("function"!=typeof g)throw TypeError(t+" is not iterable!");if(i(g)){for(d=c(t.length);d>b;b++)if(y=e?m(a(h=t[b])[0],h[1]):m(t[b]),y===u||y===f)return y}else for(v=g.call(t);!(h=v.next()).done;)if(y=o(v,m,h.value,e),y===u||y===f)return y};e.BREAK=u,e.RETURN=f},"89ca":function(t,e,n){n("b42c"),n("93c4"),t.exports=n("d38f")},"8a12":function(t,e,n){var r=n("da3c"),o=r.navigator;t.exports=o&&o.userAgent||""},"8b97":function(t,e,n){var r=n("d3f4"),o=n("cb7c"),i=function(t,e){if(o(t),!r(e)&&null!==e)throw TypeError(e+": can't set as prototype!")};t.exports={set:Object.setPrototypeOf||("__proto__"in{}?function(t,e,r){try{r=n("9b43")(Function.call,n("11e9").f(Object.prototype,"__proto__").set,2),r(t,[]),e=!(t instanceof Array)}catch(o){e=!0}return function(t,n){return i(t,n),e?t.__proto__=n:r(t,n),t}}({},!1):void 0),check:i}},"8bab":function(t,e,n){var r=n("6e1f");t.exports=Object("z").propertyIsEnumerable(0)?Object:function(t){return"String"==r(t)?t.split(""):Object(t)}},"8c4f":function(t,e,n){"use strict"; +/*! + * vue-router v3.0.2 + * (c) 2018 Evan You + * @license MIT + */function r(t,e){0}function o(t){return Object.prototype.toString.call(t).indexOf("Error")>-1}function i(t,e){for(var n in e)t[n]=e[n];return t}var a={name:"RouterView",functional:!0,props:{name:{type:String,default:"default"}},render:function(t,e){var n=e.props,r=e.children,o=e.parent,a=e.data;a.routerView=!0;var s=o.$createElement,u=n.name,f=o.$route,l=o._routerViewCache||(o._routerViewCache={}),p=0,d=!1;while(o&&o._routerRoot!==o)o.$vnode&&o.$vnode.data.routerView&&p++,o._inactive&&(d=!0),o=o.$parent;if(a.routerViewDepth=p,d)return s(l[u],a,r);var h=f.matched[p];if(!h)return l[u]=null,s();var v=l[u]=h.components[u];a.registerRouteInstance=function(t,e){var n=h.instances[u];(e&&n!==t||!e&&n===t)&&(h.instances[u]=e)},(a.hook||(a.hook={})).prepatch=function(t,e){h.instances[u]=e.componentInstance};var y=a.props=c(f,h.props&&h.props[u]);if(y){y=a.props=i({},y);var g=a.attrs=a.attrs||{};for(var m in y)v.props&&m in v.props||(g[m]=y[m],delete y[m])}return s(v,a,r)}};function c(t,e){switch(typeof e){case"undefined":return;case"object":return e;case"function":return e(t);case"boolean":return e?t.params:void 0;default:0}}var s=/[!'()*]/g,u=function(t){return"%"+t.charCodeAt(0).toString(16)},f=/%2C/g,l=function(t){return encodeURIComponent(t).replace(s,u).replace(f,",")},p=decodeURIComponent;function d(t,e,n){void 0===e&&(e={});var r,o=n||h;try{r=o(t||"")}catch(a){r={}}for(var i in e)r[i]=e[i];return r}function h(t){var e={};return t=t.trim().replace(/^(\?|#|&)/,""),t?(t.split("&").forEach(function(t){var n=t.replace(/\+/g," ").split("="),r=p(n.shift()),o=n.length>0?p(n.join("=")):null;void 0===e[r]?e[r]=o:Array.isArray(e[r])?e[r].push(o):e[r]=[e[r],o]}),e):e}function v(t){var e=t?Object.keys(t).map(function(e){var n=t[e];if(void 0===n)return"";if(null===n)return l(e);if(Array.isArray(n)){var r=[];return n.forEach(function(t){void 0!==t&&(null===t?r.push(l(e)):r.push(l(e)+"="+l(t)))}),r.join("&")}return l(e)+"="+l(n)}).filter(function(t){return t.length>0}).join("&"):null;return e?"?"+e:""}var y=/\/?$/;function g(t,e,n,r){var o=r&&r.options.stringifyQuery,i=e.query||{};try{i=m(i)}catch(c){}var a={name:e.name||t&&t.name,meta:t&&t.meta||{},path:e.path||"/",hash:e.hash||"",query:i,params:e.params||{},fullPath:w(e,o),matched:t?_(t):[]};return n&&(a.redirectedFrom=w(n,o)),Object.freeze(a)}function m(t){if(Array.isArray(t))return t.map(m);if(t&&"object"===typeof t){var e={};for(var n in t)e[n]=m(t[n]);return e}return t}var b=g(null,{path:"/"});function _(t){var e=[];while(t)e.unshift(t),t=t.parent;return e}function w(t,e){var n=t.path,r=t.query;void 0===r&&(r={});var o=t.hash;void 0===o&&(o="");var i=e||v;return(n||"/")+i(r)+o}function x(t,e){return e===b?t===e:!!e&&(t.path&&e.path?t.path.replace(y,"")===e.path.replace(y,"")&&t.hash===e.hash&&O(t.query,e.query):!(!t.name||!e.name)&&(t.name===e.name&&t.hash===e.hash&&O(t.query,e.query)&&O(t.params,e.params)))}function O(t,e){if(void 0===t&&(t={}),void 0===e&&(e={}),!t||!e)return t===e;var n=Object.keys(t),r=Object.keys(e);return n.length===r.length&&n.every(function(n){var r=t[n],o=e[n];return"object"===typeof r&&"object"===typeof o?O(r,o):String(r)===String(o)})}function S(t,e){return 0===t.path.replace(y,"/").indexOf(e.path.replace(y,"/"))&&(!e.hash||t.hash===e.hash)&&$(t.query,e.query)}function $(t,e){for(var n in e)if(!(n in t))return!1;return!0}var A,C=[String,Object],k=[String,Array],E={name:"RouterLink",props:{to:{type:C,required:!0},tag:{type:String,default:"a"},exact:Boolean,append:Boolean,replace:Boolean,activeClass:String,exactActiveClass:String,event:{type:k,default:"click"}},render:function(t){var e=this,n=this.$router,r=this.$route,o=n.resolve(this.to,r,this.append),a=o.location,c=o.route,s=o.href,u={},f=n.options.linkActiveClass,l=n.options.linkExactActiveClass,p=null==f?"router-link-active":f,d=null==l?"router-link-exact-active":l,h=null==this.activeClass?p:this.activeClass,v=null==this.exactActiveClass?d:this.exactActiveClass,y=a.path?g(null,a,null,n):c;u[v]=x(r,y),u[h]=this.exact?u[v]:S(r,y);var m=function(t){M(t)&&(e.replace?n.replace(a):n.push(a))},b={click:M};Array.isArray(this.event)?this.event.forEach(function(t){b[t]=m}):b[this.event]=m;var _={class:u};if("a"===this.tag)_.on=b,_.attrs={href:s};else{var w=j(this.$slots.default);if(w){w.isStatic=!1;var O=w.data=i({},w.data);O.on=b;var $=w.data.attrs=i({},w.data.attrs);$.href=s}else _.on=b}return t(this.tag,_,this.$slots.default)}};function M(t){if(!(t.metaKey||t.altKey||t.ctrlKey||t.shiftKey)&&!t.defaultPrevented&&(void 0===t.button||0===t.button)){if(t.currentTarget&&t.currentTarget.getAttribute){var e=t.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(e))return}return t.preventDefault&&t.preventDefault(),!0}}function j(t){if(t)for(var e,n=0;n=0&&(e=t.slice(r),t=t.slice(0,r));var o=t.indexOf("?");return o>=0&&(n=t.slice(o+1),t=t.slice(0,o)),{path:t,query:n,hash:e}}function I(t){return t.replace(/\/\//g,"/")}var N=Array.isArray||function(t){return"[object Array]"==Object.prototype.toString.call(t)},R=rt,F=U,H=Y,z=G,V=nt,B=new RegExp(["(\\\\.)","([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))"].join("|"),"g");function U(t,e){var n,r=[],o=0,i=0,a="",c=e&&e.delimiter||"/";while(null!=(n=B.exec(t))){var s=n[0],u=n[1],f=n.index;if(a+=t.slice(i,f),i=f+s.length,u)a+=u[1];else{var l=t[i],p=n[2],d=n[3],h=n[4],v=n[5],y=n[6],g=n[7];a&&(r.push(a),a="");var m=null!=p&&null!=l&&l!==p,b="+"===y||"*"===y,_="?"===y||"*"===y,w=n[2]||c,x=h||v;r.push({name:d||o++,prefix:p||"",delimiter:w,optional:_,repeat:b,partial:m,asterisk:!!g,pattern:x?X(x):g?".*":"[^"+K(w)+"]+?"})}}return i-1&&(c.params[p]=n.params[p]);if(u)return c.path=it(u.path,c.params,'named route "'+s+'"'),f(u,c,a)}else if(c.path){c.params={};for(var d=0;d=t.length?n():t[o]?e(t[o],function(){r(o+1)}):r(o+1)};r(0)}function Dt(t){return function(e,n,r){var i=!1,a=0,c=null;Lt(t,function(t,e,n,s){if("function"===typeof t&&void 0===t.cid){i=!0,a++;var u,f=Ft(function(e){Rt(e)&&(e=e.default),t.resolved="function"===typeof e?e:A.extend(e),n.components[s]=e,a--,a<=0&&r()}),l=Ft(function(t){var e="Failed to resolve async component "+s+": "+t;c||(c=o(t)?t:new Error(e),r(c))});try{u=t(f,l)}catch(d){l(d)}if(u)if("function"===typeof u.then)u.then(f,l);else{var p=u.component;p&&"function"===typeof p.then&&p.then(f,l)}}}),i||r()}}function Lt(t,e){return It(t.map(function(t){return Object.keys(t.components).map(function(n){return e(t.components[n],t.instances[n],t,n)})}))}function It(t){return Array.prototype.concat.apply([],t)}var Nt="function"===typeof Symbol&&"symbol"===typeof Symbol.toStringTag;function Rt(t){return t.__esModule||Nt&&"Module"===t[Symbol.toStringTag]}function Ft(t){var e=!1;return function(){var n=[],r=arguments.length;while(r--)n[r]=arguments[r];if(!e)return e=!0,t.apply(this,n)}}var Ht=function(t,e){this.router=t,this.base=zt(e),this.current=b,this.pending=null,this.ready=!1,this.readyCbs=[],this.readyErrorCbs=[],this.errorCbs=[]};function zt(t){if(!t)if(P){var e=document.querySelector("base");t=e&&e.getAttribute("href")||"/",t=t.replace(/^https?:\/\/[^\/]+/,"")}else t="/";return"/"!==t.charAt(0)&&(t="/"+t),t.replace(/\/$/,"")}function Vt(t,e){var n,r=Math.max(t.length,e.length);for(n=0;n=0?e.slice(0,n):e;return r+"#"+t}function oe(t){$t?jt(re(t)):window.location.hash=t}function ie(t){$t?Tt(re(t)):window.location.replace(re(t))}var ae=function(t){function e(e,n){t.call(this,e,n),this.stack=[],this.index=-1}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.push=function(t,e,n){var r=this;this.transitionTo(t,function(t){r.stack=r.stack.slice(0,r.index+1).concat(t),r.index++,e&&e(t)},n)},e.prototype.replace=function(t,e,n){var r=this;this.transitionTo(t,function(t){r.stack=r.stack.slice(0,r.index).concat(t),e&&e(t)},n)},e.prototype.go=function(t){var e=this,n=this.index+t;if(!(n<0||n>=this.stack.length)){var r=this.stack[n];this.confirmTransition(r,function(){e.index=n,e.updateRoute(r)})}},e.prototype.getCurrentLocation=function(){var t=this.stack[this.stack.length-1];return t?t.fullPath:"/"},e.prototype.ensureURL=function(){},e}(Ht),ce=function(t){void 0===t&&(t={}),this.app=null,this.apps=[],this.options=t,this.beforeHooks=[],this.resolveHooks=[],this.afterHooks=[],this.matcher=lt(t.routes||[],this);var e=t.mode||"hash";switch(this.fallback="history"===e&&!$t&&!1!==t.fallback,this.fallback&&(e="hash"),P||(e="abstract"),this.mode=e,e){case"history":this.history=new Jt(this,t.base);break;case"hash":this.history=new Qt(this,t.base,this.fallback);break;case"abstract":this.history=new ae(this,t.base);break;default:0}},se={currentRoute:{configurable:!0}};function ue(t,e){return t.push(e),function(){var n=t.indexOf(e);n>-1&&t.splice(n,1)}}function fe(t,e,n){var r="hash"===n?"#"+e:e;return t?I(t+"/"+r):r}ce.prototype.match=function(t,e,n){return this.matcher.match(t,e,n)},se.currentRoute.get=function(){return this.history&&this.history.current},ce.prototype.init=function(t){var e=this;if(this.apps.push(t),!this.app){this.app=t;var n=this.history;if(n instanceof Jt)n.transitionTo(n.getCurrentLocation());else if(n instanceof Qt){var r=function(){n.setupListeners()};n.transitionTo(n.getCurrentLocation(),r,r)}n.listen(function(t){e.apps.forEach(function(e){e._route=t})})}},ce.prototype.beforeEach=function(t){return ue(this.beforeHooks,t)},ce.prototype.beforeResolve=function(t){return ue(this.resolveHooks,t)},ce.prototype.afterEach=function(t){return ue(this.afterHooks,t)},ce.prototype.onReady=function(t,e){this.history.onReady(t,e)},ce.prototype.onError=function(t){this.history.onError(t)},ce.prototype.push=function(t,e,n){this.history.push(t,e,n)},ce.prototype.replace=function(t,e,n){this.history.replace(t,e,n)},ce.prototype.go=function(t){this.history.go(t)},ce.prototype.back=function(){this.go(-1)},ce.prototype.forward=function(){this.go(1)},ce.prototype.getMatchedComponents=function(t){var e=t?t.matched?t:this.resolve(t).route:this.currentRoute;return e?[].concat.apply([],e.matched.map(function(t){return Object.keys(t.components).map(function(e){return t.components[e]})})):[]},ce.prototype.resolve=function(t,e,n){var r=ft(t,e||this.history.current,n,this),o=this.match(r,e),i=o.redirectedFrom||o.fullPath,a=this.history.base,c=fe(a,i,this.mode);return{location:r,route:o,href:c,normalizedTo:r,resolved:o}},ce.prototype.addRoutes=function(t){this.matcher.addRoutes(t),this.history.current!==b&&this.history.transitionTo(this.history.getCurrentLocation())},Object.defineProperties(ce.prototype,se),ce.install=T,ce.version="3.0.2",P&&window.Vue&&window.Vue.use(ce),e["a"]=ce},"8ce0":function(t,e,n){var r=n("3adc"),o=n("f845");t.exports=n("7d95")?function(t,e,n){return r.f(t,e,o(1,n))}:function(t,e,n){return t[e]=n,t}},9093:function(t,e,n){var r=n("ce10"),o=n("e11e").concat("length","prototype");e.f=Object.getOwnPropertyNames||function(t){return r(t,o)}},9184:function(t,e){t.exports=function(t){if("function"!=typeof t)throw TypeError(t+" is not a function!");return t}},"91d3":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:":";return(0,r.withParams)({type:"macAddress"},function(e){if(!(0,r.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(o)})};var o=function(t){return t.toLowerCase().match(/^[0-9a-f]{2}$/)}},"93c4":function(t,e,n){"use strict";var r=n("2a4e")(!0);n("e4a9")(String,"String",function(t){this._t=String(t),this._i=0},function(){var t,e=this._t,n=this._i;return n>=e.length?{value:void 0,done:!0}:(t=r(e,n),this._i+=t.length,{value:t,done:!1})})},"9a51":function(t,e,n){var r=n("d13f");r(r.S+r.F,"Object",{assign:n("9e44")})},"9b43":function(t,e,n){var r=n("d8e8");t.exports=function(t,e,n){if(r(t),void 0===e)return t;switch(n){case 1:return function(n){return t.call(e,n)};case 2:return function(n,r){return t.call(e,n,r)};case 3:return function(n,r,o){return t.call(e,n,r,o)}}return function(){return t.apply(e,arguments)}}},"9c6c":function(t,e,n){var r=n("2b4c")("unscopables"),o=Array.prototype;void 0==o[r]&&n("32e9")(o,r,{}),t.exports=function(t){o[r][t]=!0}},"9c93":function(t,e,n){var r=n("0f89");t.exports=function(t,e,n,o){try{return o?e(r(n)[0],n[1]):e(n)}catch(a){var i=t["return"];throw void 0!==i&&r(i.call(t)),a}}},"9def":function(t,e,n){var r=n("4588"),o=Math.min;t.exports=function(t){return t>0?o(r(t),9007199254740991):0}},"9e1e":function(t,e,n){t.exports=!n("79e5")(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},"9e44":function(t,e,n){"use strict";var r=n("7633"),o=n("31c2"),i=n("d74e"),a=n("0185"),c=n("8bab"),s=Object.assign;t.exports=!s||n("d782")(function(){var t={},e={},n=Symbol(),r="abcdefghijklmnopqrst";return t[n]=7,r.split("").forEach(function(t){e[t]=t}),7!=s({},t)[n]||Object.keys(s({},e)).join("")!=r})?function(t,e){var n=a(t),s=arguments.length,u=1,f=o.f,l=i.f;while(s>u){var p,d=c(arguments[u++]),h=f?r(d).concat(f(d)):r(d),v=h.length,y=0;while(v>y)l.call(d,p=h[y++])&&(n[p]=d[p])}return n}:s},"9ed1":function(t,e,n){var r=n("f6d7"),o=n("49c1");t.exports=Object.keys||function(t){return r(t,o)}},"9faf":function(t,e,n){var r=n("1f51");t.exports=function(t,e,n){for(var o in e)r(t,o,e[o],n);return t}},a026:function(t,e,n){"use strict";(function(t){ +/*! + * Vue.js v2.6.10 + * (c) 2014-2019 Evan You + * Released under the MIT License. + */ +var n=Object.freeze({});function r(t){return void 0===t||null===t}function o(t){return void 0!==t&&null!==t}function i(t){return!0===t}function a(t){return!1===t}function c(t){return"string"===typeof t||"number"===typeof t||"symbol"===typeof t||"boolean"===typeof t}function s(t){return null!==t&&"object"===typeof t}var u=Object.prototype.toString;function f(t){return"[object Object]"===u.call(t)}function l(t){return"[object RegExp]"===u.call(t)}function p(t){var e=parseFloat(String(t));return e>=0&&Math.floor(e)===e&&isFinite(t)}function d(t){return o(t)&&"function"===typeof t.then&&"function"===typeof t.catch}function h(t){return null==t?"":Array.isArray(t)||f(t)&&t.toString===u?JSON.stringify(t,null,2):String(t)}function v(t){var e=parseFloat(t);return isNaN(e)?t:e}function y(t,e){for(var n=Object.create(null),r=t.split(","),o=0;o-1)return t.splice(n,1)}}var _=Object.prototype.hasOwnProperty;function w(t,e){return _.call(t,e)}function x(t){var e=Object.create(null);return function(n){var r=e[n];return r||(e[n]=t(n))}}var O=/-(\w)/g,S=x(function(t){return t.replace(O,function(t,e){return e?e.toUpperCase():""})}),$=x(function(t){return t.charAt(0).toUpperCase()+t.slice(1)}),A=/\B([A-Z])/g,C=x(function(t){return t.replace(A,"-$1").toLowerCase()});function k(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 E(t,e){return t.bind(e)}var M=Function.prototype.bind?E:k;function j(t,e){e=e||0;var n=t.length-e,r=new Array(n);while(n--)r[n]=t[n+e];return r}function T(t,e){for(var n in e)t[n]=e[n];return t}function P(t){for(var e={},n=0;n0,ot=et&&et.indexOf("edge/")>0,it=(et&&et.indexOf("android"),et&&/iphone|ipad|ipod|ios/.test(et)||"ios"===tt),at=(et&&/chrome\/\d+/.test(et),et&&/phantomjs/.test(et),et&&et.match(/firefox\/(\d+)/)),ct={}.watch,st=!1;if(Z)try{var ut={};Object.defineProperty(ut,"passive",{get:function(){st=!0}}),window.addEventListener("test-passive",null,ut)}catch(Ju){}var ft=function(){return void 0===X&&(X=!Z&&!Q&&"undefined"!==typeof t&&(t["process"]&&"server"===t["process"].env.VUE_ENV)),X},lt=Z&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__;function pt(t){return"function"===typeof t&&/native code/.test(t.toString())}var dt,ht="undefined"!==typeof Symbol&&pt(Symbol)&&"undefined"!==typeof Reflect&&pt(Reflect.ownKeys);dt="undefined"!==typeof Set&&pt(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 vt=D,yt=0,gt=function(){this.id=yt++,this.subs=[]};gt.prototype.addSub=function(t){this.subs.push(t)},gt.prototype.removeSub=function(t){b(this.subs,t)},gt.prototype.depend=function(){gt.target&>.target.addDep(this)},gt.prototype.notify=function(){var t=this.subs.slice();for(var e=0,n=t.length;e-1)if(i&&!w(o,"default"))a=!1;else if(""===a||a===C(t)){var s=ne(String,o.type);(s<0||c0&&(a=Ee(a,(e||"")+"_"+n),ke(a[0])&&ke(u)&&(f[s]=St(u.text+a[0].text),a.shift()),f.push.apply(f,a)):c(a)?ke(u)?f[s]=St(u.text+a):""!==a&&f.push(St(a)):ke(a)&&ke(u)?f[s]=St(u.text+a.text):(i(t._isVList)&&o(a.tag)&&r(a.key)&&o(e)&&(a.key="__vlist"+e+"_"+n+"__"),f.push(a)));return f}function Me(t){var e=t.$options.provide;e&&(t._provided="function"===typeof e?e.call(t):e)}function je(t){var e=Te(t.$options.inject,t);e&&(jt(!1),Object.keys(e).forEach(function(n){It(t,n,e[n])}),jt(!0))}function Te(t,e){if(t){for(var n=Object.create(null),r=ht?Reflect.ownKeys(t):Object.keys(t),o=0;o0,a=t?!!t.$stable:!i,c=t&&t.$key;if(t){if(t._normalized)return t._normalized;if(a&&r&&r!==n&&c===r.$key&&!i&&!r.$hasNormal)return r;for(var s in o={},t)t[s]&&"$"!==s[0]&&(o[s]=Ie(e,s,t[s]))}else o={};for(var u in e)u in o||(o[u]=Ne(e,u));return t&&Object.isExtensible(t)&&(t._normalized=o),W(o,"$stable",a),W(o,"$key",c),W(o,"$hasNormal",i),o}function Ie(t,e,n){var r=function(){var t=arguments.length?n.apply(null,arguments):n({});return t=t&&"object"===typeof t&&!Array.isArray(t)?[t]:Ce(t),t&&(0===t.length||1===t.length&&t[0].isComment)?void 0:t};return n.proxy&&Object.defineProperty(t,e,{get:r,enumerable:!0,configurable:!0}),r}function Ne(t,e){return function(){return t[e]}}function Re(t,e){var n,r,i,a,c;if(Array.isArray(t)||"string"===typeof t)for(n=new Array(t.length),r=0,i=t.length;r1?j(n):n;for(var r=j(arguments,1),o='event handler for "'+t+'"',i=0,a=n.length;idocument.createEvent("Event").timeStamp&&(Xn=function(){return Jn.now()})}function Zn(){var t,e;for(Kn=Xn(),qn=!0,Vn.sort(function(t,e){return t.id-e.id}),Wn=0;WnWn&&Vn[n].id>t.id)n--;Vn.splice(n+1,0,t)}else Vn.push(t);Yn||(Yn=!0,ye(Zn))}}var rr=0,or=function(t,e,n,r,o){this.vm=t,o&&(t._watcher=this),t._watchers.push(this),r?(this.deep=!!r.deep,this.user=!!r.user,this.lazy=!!r.lazy,this.sync=!!r.sync,this.before=r.before):this.deep=this.user=this.lazy=this.sync=!1,this.cb=n,this.id=++rr,this.active=!0,this.dirty=this.lazy,this.deps=[],this.newDeps=[],this.depIds=new dt,this.newDepIds=new dt,this.expression="","function"===typeof e?this.getter=e:(this.getter=K(e),this.getter||(this.getter=D)),this.value=this.lazy?void 0:this.get()};or.prototype.get=function(){var t;bt(this);var e=this.vm;try{t=this.getter.call(e,e)}catch(Ju){if(!this.user)throw Ju;re(Ju,e,'getter for watcher "'+this.expression+'"')}finally{this.deep&&me(t),_t(),this.cleanupDeps()}return t},or.prototype.addDep=function(t){var e=t.id;this.newDepIds.has(e)||(this.newDepIds.add(e),this.newDeps.push(t),this.depIds.has(e)||t.addSub(this))},or.prototype.cleanupDeps=function(){var t=this.deps.length;while(t--){var e=this.deps[t];this.newDepIds.has(e.id)||e.removeSub(this)}var n=this.depIds;this.depIds=this.newDepIds,this.newDepIds=n,this.newDepIds.clear(),n=this.deps,this.deps=this.newDeps,this.newDeps=n,this.newDeps.length=0},or.prototype.update=function(){this.lazy?this.dirty=!0:this.sync?this.run():nr(this)},or.prototype.run=function(){if(this.active){var t=this.get();if(t!==this.value||s(t)||this.deep){var e=this.value;if(this.value=t,this.user)try{this.cb.call(this.vm,t,e)}catch(Ju){re(Ju,this.vm,'callback for watcher "'+this.expression+'"')}else this.cb.call(this.vm,t,e)}}},or.prototype.evaluate=function(){this.value=this.get(),this.dirty=!1},or.prototype.depend=function(){var t=this.deps.length;while(t--)this.deps[t].depend()},or.prototype.teardown=function(){if(this.active){this.vm._isBeingDestroyed||b(this.vm._watchers,this);var t=this.deps.length;while(t--)this.deps[t].removeSub(this);this.active=!1}};var ir={enumerable:!0,configurable:!0,get:D,set:D};function ar(t,e,n){ir.get=function(){return this[e][n]},ir.set=function(t){this[e][n]=t},Object.defineProperty(t,n,ir)}function cr(t){t._watchers=[];var e=t.$options;e.props&&sr(t,e.props),e.methods&&yr(t,e.methods),e.data?ur(t):Lt(t._data={},!0),e.computed&&pr(t,e.computed),e.watch&&e.watch!==ct&&gr(t,e.watch)}function sr(t,e){var n=t.$options.propsData||{},r=t._props={},o=t.$options._propKeys=[],i=!t.$parent;i||jt(!1);var a=function(i){o.push(i);var a=Zt(i,e,n,t);It(r,i,a),i in t||ar(t,"_props",i)};for(var c in e)a(c);jt(!0)}function ur(t){var e=t.$options.data;e=t._data="function"===typeof e?fr(e,t):e||{},f(e)||(e={});var n=Object.keys(e),r=t.$options.props,o=(t.$options.methods,n.length);while(o--){var i=n[o];0,r&&w(r,i)||q(i)||ar(t,"_data",i)}Lt(e,!0)}function fr(t,e){bt();try{return t.call(e,e)}catch(Ju){return re(Ju,e,"data()"),{}}finally{_t()}}var lr={lazy:!0};function pr(t,e){var n=t._computedWatchers=Object.create(null),r=ft();for(var o in e){var i=e[o],a="function"===typeof i?i:i.get;0,r||(n[o]=new or(t,a||D,D,lr)),o in t||dr(t,o,i)}}function dr(t,e,n){var r=!ft();"function"===typeof n?(ir.get=r?hr(e):vr(n),ir.set=D):(ir.get=n.get?r&&!1!==n.cache?hr(e):vr(n.get):D,ir.set=n.set||D),Object.defineProperty(t,e,ir)}function hr(t){return function(){var e=this._computedWatchers&&this._computedWatchers[t];if(e)return e.dirty&&e.evaluate(),gt.target&&e.depend(),e.value}}function vr(t){return function(){return t.call(this,this)}}function yr(t,e){t.$options.props;for(var n in e)t[n]="function"!==typeof e[n]?D:M(e[n],t)}function gr(t,e){for(var n in e){var r=e[n];if(Array.isArray(r))for(var o=0;o-1)return this;var n=j(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}}function Cr(t){t.mixin=function(t){return this.options=Xt(this.options,t),this}}function kr(t){t.cid=0;var e=1;t.extend=function(t){t=t||{};var n=this,r=n.cid,o=t._Ctor||(t._Ctor={});if(o[r])return o[r];var i=t.name||n.options.name;var a=function(t){this._init(t)};return a.prototype=Object.create(n.prototype),a.prototype.constructor=a,a.cid=e++,a.options=Xt(n.options,t),a["super"]=n,a.options.props&&Er(a),a.options.computed&&Mr(a),a.extend=n.extend,a.mixin=n.mixin,a.use=n.use,V.forEach(function(t){a[t]=n[t]}),i&&(a.options.components[i]=a),a.superOptions=n.options,a.extendOptions=t,a.sealedOptions=T({},a.options),o[r]=a,a}}function Er(t){var e=t.options.props;for(var n in e)ar(t.prototype,"_props",n)}function Mr(t){var e=t.options.computed;for(var n in e)dr(t.prototype,n,e[n])}function jr(t){V.forEach(function(e){t[e]=function(t,n){return n?("component"===e&&f(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]}})}function Tr(t){return t&&(t.Ctor.options.name||t.tag)}function Pr(t,e){return Array.isArray(t)?t.indexOf(e)>-1:"string"===typeof t?t.split(",").indexOf(e)>-1:!!l(t)&&t.test(e)}function Dr(t,e){var n=t.cache,r=t.keys,o=t._vnode;for(var i in n){var a=n[i];if(a){var c=Tr(a.componentOptions);c&&!e(c)&&Lr(n,i,r,o)}}}function Lr(t,e,n,r){var o=t[e];!o||r&&o.tag===r.tag||o.componentInstance.$destroy(),t[e]=null,b(n,e)}wr($r),br($r),jn($r),Ln($r),_n($r);var Ir=[String,RegExp,Array],Nr={name:"keep-alive",abstract:!0,props:{include:Ir,exclude:Ir,max:[String,Number]},created:function(){this.cache=Object.create(null),this.keys=[]},destroyed:function(){for(var t in this.cache)Lr(this.cache,t,this.keys)},mounted:function(){var t=this;this.$watch("include",function(e){Dr(t,function(t){return Pr(e,t)})}),this.$watch("exclude",function(e){Dr(t,function(t){return!Pr(e,t)})})},render:function(){var t=this.$slots.default,e=$n(t),n=e&&e.componentOptions;if(n){var r=Tr(n),o=this,i=o.include,a=o.exclude;if(i&&(!r||!Pr(i,r))||a&&r&&Pr(a,r))return e;var c=this,s=c.cache,u=c.keys,f=null==e.key?n.Ctor.cid+(n.tag?"::"+n.tag:""):e.key;s[f]?(e.componentInstance=s[f].componentInstance,b(u,f),u.push(f)):(s[f]=e,u.push(f),this.max&&u.length>parseInt(this.max)&&Lr(s,u[0],u,this._vnode)),e.data.keepAlive=!0}return e||t&&t[0]}},Rr={KeepAlive:Nr};function Fr(t){var e={get:function(){return U}};Object.defineProperty(t,"config",e),t.util={warn:vt,extend:T,mergeOptions:Xt,defineReactive:It},t.set=Nt,t.delete=Rt,t.nextTick=ye,t.observable=function(t){return Lt(t),t},t.options=Object.create(null),V.forEach(function(e){t.options[e+"s"]=Object.create(null)}),t.options._base=t,T(t.options.components,Rr),Ar(t),Cr(t),kr(t),jr(t)}Fr($r),Object.defineProperty($r.prototype,"$isServer",{get:ft}),Object.defineProperty($r.prototype,"$ssrContext",{get:function(){return this.$vnode&&this.$vnode.ssrContext}}),Object.defineProperty($r,"FunctionalRenderContext",{value:Qe}),$r.version="2.6.10";var Hr=y("style,class"),zr=y("input,textarea,option,select,progress"),Vr=function(t,e,n){return"value"===n&&zr(t)&&"button"!==e||"selected"===n&&"option"===t||"checked"===n&&"input"===t||"muted"===n&&"video"===t},Br=y("contenteditable,draggable,spellcheck"),Ur=y("events,caret,typing,plaintext-only"),Yr=function(t,e){return Xr(e)||"false"===e?"false":"contenteditable"===t&&Ur(e)?e:"true"},qr=y("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,translate,truespeed,typemustmatch,visible"),Wr="http://www.w3.org/1999/xlink",Gr=function(t){return":"===t.charAt(5)&&"xlink"===t.slice(0,5)},Kr=function(t){return Gr(t)?t.slice(6,t.length):""},Xr=function(t){return null==t||!1===t};function Jr(t){var e=t.data,n=t,r=t;while(o(r.componentInstance))r=r.componentInstance._vnode,r&&r.data&&(e=Zr(r.data,e));while(o(n=n.parent))n&&n.data&&(e=Zr(e,n.data));return Qr(e.staticClass,e.class)}function Zr(t,e){return{staticClass:to(t.staticClass,e.staticClass),class:o(t.class)?[t.class,e.class]:e.class}}function Qr(t,e){return o(t)||o(e)?to(t,eo(e)):""}function to(t,e){return t?e?t+" "+e:t:e||""}function eo(t){return Array.isArray(t)?no(t):s(t)?ro(t):"string"===typeof t?t:""}function no(t){for(var e,n="",r=0,i=t.length;r-1?fo[t]=e.constructor===window.HTMLUnknownElement||e.constructor===window.HTMLElement:fo[t]=/HTMLUnknownElement/.test(e.toString())}var po=y("text,number,password,search,email,tel,url");function ho(t){if("string"===typeof t){var e=document.querySelector(t);return e||document.createElement("div")}return t}function vo(t,e){var n=document.createElement(t);return"select"!==t?n:(e.data&&e.data.attrs&&void 0!==e.data.attrs.multiple&&n.setAttribute("multiple","multiple"),n)}function yo(t,e){return document.createElementNS(oo[t],e)}function go(t){return document.createTextNode(t)}function mo(t){return document.createComment(t)}function bo(t,e,n){t.insertBefore(e,n)}function _o(t,e){t.removeChild(e)}function wo(t,e){t.appendChild(e)}function xo(t){return t.parentNode}function Oo(t){return t.nextSibling}function So(t){return t.tagName}function $o(t,e){t.textContent=e}function Ao(t,e){t.setAttribute(e,"")}var Co=Object.freeze({createElement:vo,createElementNS:yo,createTextNode:go,createComment:mo,insertBefore:bo,removeChild:_o,appendChild:wo,parentNode:xo,nextSibling:Oo,tagName:So,setTextContent:$o,setStyleScope:Ao}),ko={create:function(t,e){Eo(e)},update:function(t,e){t.data.ref!==e.data.ref&&(Eo(t,!0),Eo(e))},destroy:function(t){Eo(t,!0)}};function Eo(t,e){var n=t.data.ref;if(o(n)){var r=t.context,i=t.componentInstance||t.elm,a=r.$refs;e?Array.isArray(a[n])?b(a[n],i):a[n]===i&&(a[n]=void 0):t.data.refInFor?Array.isArray(a[n])?a[n].indexOf(i)<0&&a[n].push(i):a[n]=[i]:a[n]=i}}var Mo=new wt("",{},[]),jo=["create","activate","update","remove","destroy"];function To(t,e){return t.key===e.key&&(t.tag===e.tag&&t.isComment===e.isComment&&o(t.data)===o(e.data)&&Po(t,e)||i(t.isAsyncPlaceholder)&&t.asyncFactory===e.asyncFactory&&r(e.asyncFactory.error))}function Po(t,e){if("input"!==t.tag)return!0;var n,r=o(n=t.data)&&o(n=n.attrs)&&n.type,i=o(n=e.data)&&o(n=n.attrs)&&n.type;return r===i||po(r)&&po(i)}function Do(t,e,n){var r,i,a={};for(r=e;r<=n;++r)i=t[r].key,o(i)&&(a[i]=r);return a}function Lo(t){var e,n,a={},s=t.modules,u=t.nodeOps;for(e=0;ev?(l=r(n[m+1])?null:n[m+1].elm,O(t,l,n,h,m,i)):h>m&&$(t,e,p,v)}function k(t,e,n,r){for(var i=n;i-1?qo(t,e,n):qr(e)?Xr(n)?t.removeAttribute(e):(n="allowfullscreen"===e&&"EMBED"===t.tagName?"true":e,t.setAttribute(e,n)):Br(e)?t.setAttribute(e,Yr(e,n)):Gr(e)?Xr(n)?t.removeAttributeNS(Wr,Kr(e)):t.setAttributeNS(Wr,e,n):qo(t,e,n)}function qo(t,e,n){if(Xr(n))t.removeAttribute(e);else{if(nt&&!rt&&"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 Wo={create:Uo,update:Uo};function Go(t,e){var n=e.elm,i=e.data,a=t.data;if(!(r(i.staticClass)&&r(i.class)&&(r(a)||r(a.staticClass)&&r(a.class)))){var c=Jr(e),s=n._transitionClasses;o(s)&&(c=to(c,eo(s))),c!==n._prevClass&&(n.setAttribute("class",c),n._prevClass=c)}}var Ko,Xo,Jo,Zo,Qo,ti,ei={create:Go,update:Go},ni=/[\w).+\-_$\]]/;function ri(t){var e,n,r,o,i,a=!1,c=!1,s=!1,u=!1,f=0,l=0,p=0,d=0;for(r=0;r=0;h--)if(v=t.charAt(h)," "!==v)break;v&&ni.test(v)||(u=!0)}}else void 0===o?(d=r+1,o=t.slice(0,r).trim()):y();function y(){(i||(i=[])).push(t.slice(d,r).trim()),d=r+1}if(void 0===o?o=t.slice(0,r).trim():0!==d&&y(),i)for(r=0;r-1?{exp:t.slice(0,Zo),key:'"'+t.slice(Zo+1)+'"'}:{exp:t,key:null};Xo=t,Zo=Qo=ti=0;while(!xi())Jo=wi(),Oi(Jo)?$i(Jo):91===Jo&&Si(Jo);return{exp:t.slice(0,Qo),key:t.slice(Qo+1,ti)}}function wi(){return Xo.charCodeAt(++Zo)}function xi(){return Zo>=Ko}function Oi(t){return 34===t||39===t}function Si(t){var e=1;Qo=Zo;while(!xi())if(t=wi(),Oi(t))$i(t);else if(91===t&&e++,93===t&&e--,0===e){ti=Zo;break}}function $i(t){var e=t;while(!xi())if(t=wi(),t===e)break}var Ai,Ci="__r",ki="__c";function Ei(t,e,n){n;var r=e.value,o=e.modifiers,i=t.tag,a=t.attrsMap.type;if(t.component)return mi(t,r,o),!1;if("select"===i)Ti(t,r,o);else if("input"===i&&"checkbox"===a)Mi(t,r,o);else if("input"===i&&"radio"===a)ji(t,r,o);else if("input"===i||"textarea"===i)Pi(t,r,o);else{if(!U.isReservedTag(i))return mi(t,r,o),!1}return!0}function Mi(t,e,n){var r=n&&n.number,o=hi(t,"value")||"null",i=hi(t,"true-value")||"true",a=hi(t,"false-value")||"false";ci(t,"checked","Array.isArray("+e+")?_i("+e+","+o+")>-1"+("true"===i?":("+e+")":":_q("+e+","+i+")")),pi(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&&("+bi(e,"$$a.concat([$$v])")+")}else{$$i>-1&&("+bi(e,"$$a.slice(0,$$i).concat($$a.slice($$i+1))")+")}}else{"+bi(e,"$$c")+"}",null,!0)}function ji(t,e,n){var r=n&&n.number,o=hi(t,"value")||"null";o=r?"_n("+o+")":o,ci(t,"checked","_q("+e+","+o+")"),pi(t,"change",bi(e,o),null,!0)}function Ti(t,e,n){var r=n&&n.number,o='Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = "_value" in o ? o._value : o.value;return '+(r?"_n(val)":"val")+"})",i="$event.target.multiple ? $$selectedVal : $$selectedVal[0]",a="var $$selectedVal = "+o+";";a=a+" "+bi(e,i),pi(t,"change",a,null,!0)}function Pi(t,e,n){var r=t.attrsMap.type,o=n||{},i=o.lazy,a=o.number,c=o.trim,s=!i&&"range"!==r,u=i?"change":"range"===r?Ci:"input",f="$event.target.value";c&&(f="$event.target.value.trim()"),a&&(f="_n("+f+")");var l=bi(e,f);s&&(l="if($event.target.composing)return;"+l),ci(t,"value","("+e+")"),pi(t,u,l,null,!0),(c||a)&&pi(t,"blur","$forceUpdate()")}function Di(t){if(o(t[Ci])){var e=nt?"change":"input";t[e]=[].concat(t[Ci],t[e]||[]),delete t[Ci]}o(t[ki])&&(t.change=[].concat(t[ki],t.change||[]),delete t[ki])}function Li(t,e,n){var r=Ai;return function o(){var i=e.apply(null,arguments);null!==i&&Ri(t,o,n,r)}}var Ii=se&&!(at&&Number(at[1])<=53);function Ni(t,e,n,r){if(Ii){var o=Kn,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)}}Ai.addEventListener(t,e,st?{capture:n,passive:r}:n)}function Ri(t,e,n,r){(r||Ai).removeEventListener(t,e._wrapper||e,n)}function Fi(t,e){if(!r(t.data.on)||!r(e.data.on)){var n=e.data.on||{},o=t.data.on||{};Ai=e.elm,Di(n),xe(n,o,Ni,Ri,Li,e.context),Ai=void 0}}var Hi,zi={create:Fi,update:Fi};function Vi(t,e){if(!r(t.data.domProps)||!r(e.data.domProps)){var n,i,a=e.elm,c=t.data.domProps||{},s=e.data.domProps||{};for(n in o(s.__ob__)&&(s=e.data.domProps=T({},s)),c)n in s||(a[n]="");for(n in s){if(i=s[n],"textContent"===n||"innerHTML"===n){if(e.children&&(e.children.length=0),i===c[n])continue;1===a.childNodes.length&&a.removeChild(a.childNodes[0])}if("value"===n&&"PROGRESS"!==a.tagName){a._value=i;var u=r(i)?"":String(i);Bi(a,u)&&(a.value=u)}else if("innerHTML"===n&&ao(a.tagName)&&r(a.innerHTML)){Hi=Hi||document.createElement("div"),Hi.innerHTML=""+i+"";var f=Hi.firstChild;while(a.firstChild)a.removeChild(a.firstChild);while(f.firstChild)a.appendChild(f.firstChild)}else if(i!==c[n])try{a[n]=i}catch(Ju){}}}}function Bi(t,e){return!t.composing&&("OPTION"===t.tagName||Ui(t,e)||Yi(t,e))}function Ui(t,e){var n=!0;try{n=document.activeElement!==t}catch(Ju){}return n&&t.value!==e}function Yi(t,e){var n=t.value,r=t._vModifiers;if(o(r)){if(r.number)return v(n)!==v(e);if(r.trim)return n.trim()!==e.trim()}return n!==e}var qi={create:Vi,update:Vi},Wi=x(function(t){var e={},n=/;(?![^(]*\))/g,r=/:(.+)/;return t.split(n).forEach(function(t){if(t){var n=t.split(r);n.length>1&&(e[n[0].trim()]=n[1].trim())}}),e});function Gi(t){var e=Ki(t.style);return t.staticStyle?T(t.staticStyle,e):e}function Ki(t){return Array.isArray(t)?P(t):"string"===typeof t?Wi(t):t}function Xi(t,e){var n,r={};if(e){var o=t;while(o.componentInstance)o=o.componentInstance._vnode,o&&o.data&&(n=Gi(o.data))&&T(r,n)}(n=Gi(t.data))&&T(r,n);var i=t;while(i=i.parent)i.data&&(n=Gi(i.data))&&T(r,n);return r}var Ji,Zi=/^--/,Qi=/\s*!important$/,ta=function(t,e,n){if(Zi.test(e))t.style.setProperty(e,n);else if(Qi.test(n))t.style.setProperty(C(e),n.replace(Qi,""),"important");else{var r=na(e);if(Array.isArray(n))for(var o=0,i=n.length;o-1?e.split(ia).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 ca(t,e){if(e&&(e=e.trim()))if(t.classList)e.indexOf(" ")>-1?e.split(ia).forEach(function(e){return t.classList.remove(e)}):t.classList.remove(e),t.classList.length||t.removeAttribute("class");else{var n=" "+(t.getAttribute("class")||"")+" ",r=" "+e+" ";while(n.indexOf(r)>=0)n=n.replace(r," ");n=n.trim(),n?t.setAttribute("class",n):t.removeAttribute("class")}}function sa(t){if(t){if("object"===typeof t){var e={};return!1!==t.css&&T(e,ua(t.name||"v")),T(e,t),e}return"string"===typeof t?ua(t):void 0}}var ua=x(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"}}),fa=Z&&!rt,la="transition",pa="animation",da="transition",ha="transitionend",va="animation",ya="animationend";fa&&(void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend&&(da="WebkitTransition",ha="webkitTransitionEnd"),void 0===window.onanimationend&&void 0!==window.onwebkitanimationend&&(va="WebkitAnimation",ya="webkitAnimationEnd"));var ga=Z?window.requestAnimationFrame?window.requestAnimationFrame.bind(window):setTimeout:function(t){return t()};function ma(t){ga(function(){ga(t)})}function ba(t,e){var n=t._transitionClasses||(t._transitionClasses=[]);n.indexOf(e)<0&&(n.push(e),aa(t,e))}function _a(t,e){t._transitionClasses&&b(t._transitionClasses,e),ca(t,e)}function wa(t,e,n){var r=Oa(t,e),o=r.type,i=r.timeout,a=r.propCount;if(!o)return n();var c=o===la?ha:ya,s=0,u=function(){t.removeEventListener(c,f),n()},f=function(e){e.target===t&&++s>=a&&u()};setTimeout(function(){s0&&(n=la,f=a,l=i.length):e===pa?u>0&&(n=pa,f=u,l=s.length):(f=Math.max(a,u),n=f>0?a>u?la:pa:null,l=n?n===la?i.length:s.length:0);var p=n===la&&xa.test(r[da+"Property"]);return{type:n,timeout:f,propCount:l,hasTransform:p}}function Sa(t,e){while(t.length1}function Ma(t,e){!0!==e.data.show&&Aa(e)}var ja=Z?{create:Ma,activate:Ma,remove:function(t,e){!0!==t.data.show?Ca(t,e):e()}}:{},Ta=[Wo,ei,zi,qi,oa,ja],Pa=Ta.concat(Bo),Da=Lo({nodeOps:Co,modules:Pa});rt&&document.addEventListener("selectionchange",function(){var t=document.activeElement;t&&t.vmodel&&Va(t,"input")});var La={inserted:function(t,e,n,r){"select"===n.tag?(r.elm&&!r.elm._vOptions?Oe(n,"postpatch",function(){La.componentUpdated(t,e,n)}):Ia(t,e,n.context),t._vOptions=[].map.call(t.options,Fa)):("textarea"===n.tag||po(t.type))&&(t._vModifiers=e.modifiers,e.modifiers.lazy||(t.addEventListener("compositionstart",Ha),t.addEventListener("compositionend",za),t.addEventListener("change",za),rt&&(t.vmodel=!0)))},componentUpdated:function(t,e,n){if("select"===n.tag){Ia(t,e,n.context);var r=t._vOptions,o=t._vOptions=[].map.call(t.options,Fa);if(o.some(function(t,e){return!R(t,r[e])})){var i=t.multiple?e.value.some(function(t){return Ra(t,o)}):e.value!==e.oldValue&&Ra(e.value,o);i&&Va(t,"change")}}}};function Ia(t,e,n){Na(t,e,n),(nt||ot)&&setTimeout(function(){Na(t,e,n)},0)}function Na(t,e,n){var r=e.value,o=t.multiple;if(!o||Array.isArray(r)){for(var i,a,c=0,s=t.options.length;c-1,a.selected!==i&&(a.selected=i);else if(R(Fa(a),r))return void(t.selectedIndex!==c&&(t.selectedIndex=c));o||(t.selectedIndex=-1)}}function Ra(t,e){return e.every(function(e){return!R(e,t)})}function Fa(t){return"_value"in t?t._value:t.value}function Ha(t){t.target.composing=!0}function za(t){t.target.composing&&(t.target.composing=!1,Va(t.target,"input"))}function Va(t,e){var n=document.createEvent("HTMLEvents");n.initEvent(e,!0,!0),t.dispatchEvent(n)}function Ba(t){return!t.componentInstance||t.data&&t.data.transition?t:Ba(t.componentInstance._vnode)}var Ua={bind:function(t,e,n){var r=e.value;n=Ba(n);var o=n.data&&n.data.transition,i=t.__vOriginalDisplay="none"===t.style.display?"":t.style.display;r&&o?(n.data.show=!0,Aa(n,function(){t.style.display=i})):t.style.display=r?i:"none"},update:function(t,e,n){var r=e.value,o=e.oldValue;if(!r!==!o){n=Ba(n);var i=n.data&&n.data.transition;i?(n.data.show=!0,r?Aa(n,function(){t.style.display=t.__vOriginalDisplay}):Ca(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)}},Ya={model:La,show:Ua},qa={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 Wa(t){var e=t&&t.componentOptions;return e&&e.Ctor.options.abstract?Wa($n(e.children)):t}function Ga(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[S(i)]=o[i];return e}function Ka(t,e){if(/\d-keep-alive$/.test(e.tag))return t("keep-alive",{props:e.componentOptions.propsData})}function Xa(t){while(t=t.parent)if(t.data.transition)return!0}function Ja(t,e){return e.key===t.key&&e.tag===t.tag}var Za=function(t){return t.tag||Sn(t)},Qa=function(t){return"show"===t.name},tc={name:"transition",props:qa,abstract:!0,render:function(t){var e=this,n=this.$slots.default;if(n&&(n=n.filter(Za),n.length)){0;var r=this.mode;0;var o=n[0];if(Xa(this.$vnode))return o;var i=Wa(o);if(!i)return o;if(this._leaving)return Ka(t,o);var a="__transition-"+this._uid+"-";i.key=null==i.key?i.isComment?a+"comment":a+i.tag:c(i.key)?0===String(i.key).indexOf(a)?i.key:a+i.key:i.key;var s=(i.data||(i.data={})).transition=Ga(this),u=this._vnode,f=Wa(u);if(i.data.directives&&i.data.directives.some(Qa)&&(i.data.show=!0),f&&f.data&&!Ja(i,f)&&!Sn(f)&&(!f.componentInstance||!f.componentInstance._vnode.isComment)){var l=f.data.transition=T({},s);if("out-in"===r)return this._leaving=!0,Oe(l,"afterLeave",function(){e._leaving=!1,e.$forceUpdate()}),Ka(t,o);if("in-out"===r){if(Sn(i))return u;var p,d=function(){p()};Oe(s,"afterEnter",d),Oe(s,"enterCancelled",d),Oe(l,"delayLeave",function(t){p=t})}}return o}}},ec=T({tag:String,moveClass:String},qa);delete ec.mode;var nc={props:ec,beforeMount:function(){var t=this,e=this._update;this._update=function(n,r){var o=Pn(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=Ga(this),c=0;cs&&(c.push(i=t.slice(s,o)),a.push(JSON.stringify(i)));var u=ri(r[1].trim());a.push("_s("+u+")"),c.push({"@binding":u}),s=o+r[0].length}return s\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,Oc=/^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,Sc="[a-zA-Z_][\\-\\.0-9_a-zA-Z"+Y.source+"]*",$c="((?:"+Sc+"\\:)?"+Sc+")",Ac=new RegExp("^<"+$c),Cc=/^\s*(\/?)>/,kc=new RegExp("^<\\/"+$c+"[^>]*>"),Ec=/^]+>/i,Mc=/^",""":'"',"&":"&"," ":"\n"," ":"\t","'":"'"},Lc=/&(?:lt|gt|quot|amp|#39);/g,Ic=/&(?:lt|gt|quot|amp|#39|#10|#9);/g,Nc=y("pre,textarea",!0),Rc=function(t,e){return t&&Nc(t)&&"\n"===e[0]};function Fc(t,e){var n=e?Ic:Lc;return t.replace(n,function(t){return Dc[t]})}function Hc(t,e){var n,r,o=[],i=e.expectHTML,a=e.isUnaryTag||L,c=e.canBeLeftOpenTag||L,s=0;while(t){if(n=t,r&&Tc(r)){var u=0,f=r.toLowerCase(),l=Pc[f]||(Pc[f]=new RegExp("([\\s\\S]*?)(]*>)","i")),p=t.replace(l,function(t,n,r){return u=r.length,Tc(f)||"noscript"===f||(n=n.replace(//g,"$1").replace(//g,"$1")),Rc(f,n)&&(n=n.slice(1)),e.chars&&e.chars(n),""});s+=t.length-p.length,t=p,A(f,s-u,s)}else{var d=t.indexOf("<");if(0===d){if(Mc.test(t)){var h=t.indexOf("--\x3e");if(h>=0){e.shouldKeepComment&&e.comment(t.substring(4,h),s,s+h+3),O(h+3);continue}}if(jc.test(t)){var v=t.indexOf("]>");if(v>=0){O(v+2);continue}}var y=t.match(Ec);if(y){O(y[0].length);continue}var g=t.match(kc);if(g){var m=s;O(g[0].length),A(g[1],m,s);continue}var b=S();if(b){$(b),Rc(b.tagName,t)&&O(1);continue}}var _=void 0,w=void 0,x=void 0;if(d>=0){w=t.slice(d);while(!kc.test(w)&&!Ac.test(w)&&!Mc.test(w)&&!jc.test(w)){if(x=w.indexOf("<",1),x<0)break;d+=x,w=t.slice(d)}_=t.substring(0,d)}d<0&&(_=t),_&&O(_.length),e.chars&&_&&e.chars(_,s-_.length,s)}if(t===n){e.chars&&e.chars(t);break}}function O(e){s+=e,t=t.substring(e)}function S(){var e=t.match(Ac);if(e){var n,r,o={tagName:e[1],attrs:[],start:s};O(e[0].length);while(!(n=t.match(Cc))&&(r=t.match(Oc)||t.match(xc)))r.start=s,O(r[0].length),r.end=s,o.attrs.push(r);if(n)return o.unarySlash=n[1],O(n[0].length),o.end=s,o}}function $(t){var n=t.tagName,s=t.unarySlash;i&&("p"===r&&wc(n)&&A(r),c(n)&&r===n&&A(n));for(var u=a(n)||!!s,f=t.attrs.length,l=new Array(f),p=0;p=0;a--)if(o[a].lowerCasedTag===c)break}else a=0;if(a>=0){for(var u=o.length-1;u>=a;u--)e.end&&e.end(o[u].tag,n,i);o.length=a,r=a&&o[a-1].tag}else"br"===c?e.start&&e.start(t,[],!0,n,i):"p"===c&&(e.start&&e.start(t,[],!1,n,i),e.end&&e.end(t,n,i))}A()}var zc,Vc,Bc,Uc,Yc,qc,Wc,Gc,Kc=/^@|^v-on:/,Xc=/^v-|^@|^:/,Jc=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,Zc=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,Qc=/^\(|\)$/g,ts=/^\[.*\]$/,es=/:(.*)$/,ns=/^:|^\.|^v-bind:/,rs=/\.[^.\]]+(?=[^\]]*$)/g,os=/^v-slot(:|$)|^#/,is=/[\r\n]/,as=/\s+/g,cs=x(mc.decode),ss="_empty_";function us(t,e,n){return{type:1,tag:t,attrsList:e,attrsMap:Ms(e),rawAttrsMap:{},parent:n,children:[]}}function fs(t,e){zc=e.warn||ii,qc=e.isPreTag||L,Wc=e.mustUseProp||L,Gc=e.getTagNamespace||L;var n=e.isReservedTag||L;(function(t){return!!t.component||!n(t.tag)}),Bc=ai(e.modules,"transformNode"),Uc=ai(e.modules,"preTransformNode"),Yc=ai(e.modules,"postTransformNode"),Vc=e.delimiters;var r,o,i=[],a=!1!==e.preserveWhitespace,c=e.whitespace,s=!1,u=!1;function f(t){if(l(t),s||t.processed||(t=ds(t,e)),i.length||t===r||r.if&&(t.elseif||t.else)&&ws(r,{exp:t.elseif,block:t}),o&&!t.forbidden)if(t.elseif||t.else)bs(t,o);else{if(t.slotScope){var n=t.slotTarget||'"default"';(o.scopedSlots||(o.scopedSlots={}))[n]=t}o.children.push(t),t.parent=o}t.children=t.children.filter(function(t){return!t.slotScope}),l(t),t.pre&&(s=!1),qc(t.tag)&&(u=!1);for(var a=0;a|^function\s*(?:[\w$]+)?\s*\(/,tu=/\([^)]*?\);*$/,eu=/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/,nu={esc:27,tab:9,enter:13,space:32,up:38,left:37,right:39,down:40,delete:[8,46]},ru={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"]},ou=function(t){return"if("+t+")return null;"},iu={stop:"$event.stopPropagation();",prevent:"$event.preventDefault();",self:ou("$event.target !== $event.currentTarget"),ctrl:ou("!$event.ctrlKey"),shift:ou("!$event.shiftKey"),alt:ou("!$event.altKey"),meta:ou("!$event.metaKey"),left:ou("'button' in $event && $event.button !== 0"),middle:ou("'button' in $event && $event.button !== 1"),right:ou("'button' in $event && $event.button !== 2")};function au(t,e){var n=e?"nativeOn:":"on:",r="",o="";for(var i in t){var a=cu(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 cu(t){if(!t)return"function(){}";if(Array.isArray(t))return"["+t.map(function(t){return cu(t)}).join(",")+"]";var e=eu.test(t.value),n=Qs.test(t.value),r=eu.test(t.value.replace(tu,""));if(t.modifiers){var o="",i="",a=[];for(var c in t.modifiers)if(iu[c])i+=iu[c],nu[c]&&a.push(c);else if("exact"===c){var s=t.modifiers;i+=ou(["ctrl","shift","alt","meta"].filter(function(t){return!s[t]}).map(function(t){return"$event."+t+"Key"}).join("||"))}else a.push(c);a.length&&(o+=su(a)),i&&(o+=i);var u=e?"return "+t.value+"($event)":n?"return ("+t.value+")($event)":r?"return "+t.value:t.value;return"function($event){"+o+u+"}"}return e||n?t.value:"function($event){"+(r?"return "+t.value:t.value)+"}"}function su(t){return"if(!$event.type.indexOf('key')&&"+t.map(uu).join("&&")+")return null;"}function uu(t){var e=parseInt(t,10);if(e)return"$event.keyCode!=="+e;var n=nu[t],r=ru[t];return"_k($event.keyCode,"+JSON.stringify(t)+","+JSON.stringify(n)+",$event.key,"+JSON.stringify(r)+")"}function fu(t,e){t.wrapListeners=function(t){return"_g("+t+","+e.value+")"}}function lu(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":"")+")"}}var pu={on:fu,bind:lu,cloak:D},du=function(t){this.options=t,this.warn=t.warn||ii,this.transforms=ai(t.modules,"transformCode"),this.dataGenFns=ai(t.modules,"genData"),this.directives=T(T({},pu),t.directives);var e=t.isReservedTag||L;this.maybeComponent=function(t){return!!t.component||!e(t.tag)},this.onceId=0,this.staticRenderFns=[],this.pre=!1};function hu(t,e){var n=new du(e),r=t?vu(t,n):'_c("div")';return{render:"with(this){return "+r+"}",staticRenderFns:n.staticRenderFns}}function vu(t,e){if(t.parent&&(t.pre=t.pre||t.parent.pre),t.staticRoot&&!t.staticProcessed)return yu(t,e);if(t.once&&!t.onceProcessed)return gu(t,e);if(t.for&&!t.forProcessed)return _u(t,e);if(t.if&&!t.ifProcessed)return mu(t,e);if("template"!==t.tag||t.slotTarget||e.pre){if("slot"===t.tag)return Du(t,e);var n;if(t.component)n=Lu(t.component,t,e);else{var r;(!t.plain||t.pre&&e.maybeComponent(t))&&(r=wu(t,e));var o=t.inlineTemplate?null:ku(t,e,!0);n="_c('"+t.tag+"'"+(r?","+r:"")+(o?","+o:"")+")"}for(var i=0;i>>0}function Au(t){return 1===t.type&&("slot"===t.tag||t.children.some(Au))}function Cu(t,e){var n=t.attrsMap["slot-scope"];if(t.if&&!t.ifProcessed&&!n)return mu(t,e,Cu,"null");if(t.for&&!t.forProcessed)return _u(t,e,Cu);var r=t.slotScope===ss?"":String(t.slotScope),o="function("+r+"){return "+("template"===t.tag?t.if&&n?"("+t.if+")?"+(ku(t,e)||"undefined")+":undefined":ku(t,e)||"undefined":vu(t,e))+"}",i=r?"":",proxy:true";return"{key:"+(t.slotTarget||'"default"')+",fn:"+o+i+"}"}function ku(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 c=n?e.maybeComponent(a)?",1":",0":"";return""+(r||vu)(a,e)+c}var s=n?Eu(i,e.maybeComponent):0,u=o||ju;return"["+i.map(function(t){return u(t,e)}).join(",")+"]"+(s?","+s:"")}}function Eu(t,e){for(var n=0,r=0;r':'
',zu.innerHTML.indexOf(" ")>0}var qu=!!Z&&Yu(!1),Wu=!!Z&&Yu(!0),Gu=x(function(t){var e=ho(t);return e&&e.innerHTML}),Ku=$r.prototype.$mount;function Xu(t){if(t.outerHTML)return t.outerHTML;var e=document.createElement("div");return e.appendChild(t.cloneNode(!0)),e.innerHTML}$r.prototype.$mount=function(t,e){if(t=t&&ho(t),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=Gu(r));else{if(!r.nodeType)return this;r=r.innerHTML}else t&&(r=Xu(t));if(r){0;var o=Uu(r,{outputSourceRange:!1,shouldDecodeNewlines:qu,shouldDecodeNewlinesForHref:Wu,delimiters:n.delimiters,comments:n.comments},this),i=o.render,a=o.staticRenderFns;n.render=i,n.staticRenderFns=a}}return Ku.call(this,t,e)},$r.compile=Uu,e["a"]=$r}).call(this,n("c8ba"))},a0a8:function(t,e,n){var r=n("e4e1"),o=n("dcc3");t.exports=n("45e2")?function(t,e,n){return r.f(t,e,o(1,n))}:function(t,e,n){return t[e]=n,t}},a243:function(t,e,n){var r=n("d13f");r(r.S,"Number",{isInteger:n("3457")})},a24c:function(t,e,n){var r=n("b808"),o=n("1ad4").set,i=r.MutationObserver||r.WebKitMutationObserver,a=r.process,c=r.Promise,s="process"==n("71fa")(a);t.exports=function(){var t,e,n,u=function(){var r,o;s&&(r=a.domain)&&r.exit();while(t){o=t.fn,t=t.next;try{o()}catch(i){throw t?n():e=void 0,i}}e=void 0,r&&r.enter()};if(s)n=function(){a.nextTick(u)};else if(!i||r.navigator&&r.navigator.standalone)if(c&&c.resolve){var f=c.resolve(void 0);n=function(){f.then(u)}}else n=function(){o.call(r,u)};else{var l=!0,p=document.createTextNode("");new i(u).observe(p,{characterData:!0}),n=function(){p.data=l=!l}}return function(r){var o={fn:r,next:void 0};e&&(e.next=o),t||(t=o,n()),e=o}}},a274:function(t,e,n){var r=n("71fa"),o=n("b67f")("toStringTag"),i="Arguments"==r(function(){return arguments}()),a=function(t,e){try{return t[e]}catch(n){}};t.exports=function(t){var e,n,c;return void 0===t?"Undefined":null===t?"Null":"string"==typeof(n=a(e=Object(t),o))?n:i?r(e):"Object"==(c=r(e))&&"function"==typeof e.callee?"Arguments":c}},a402:function(t,e,n){"use strict";var r=n("9ed1"),o=n("696b"),i=n("bf41"),a=n("4cf4"),c=n("d43f"),s=Object.assign;t.exports=!s||n("b629")(function(){var t={},e={},n=Symbol(),r="abcdefghijklmnopqrst";return t[n]=7,r.split("").forEach(function(t){e[t]=t}),7!=s({},t)[n]||Object.keys(s({},e)).join("")!=r})?function(t,e){var n=a(t),s=arguments.length,u=1,f=o.f,l=i.f;while(s>u){var p,d=c(arguments[u++]),h=f?r(d).concat(f(d)):r(d),v=h.length,y=0;while(v>y)l.call(d,p=h[y++])&&(n[p]=d[p])}return n}:s},a47f:function(t,e,n){t.exports=!n("7d95")&&!n("d782")(function(){return 7!=Object.defineProperty(n("12fd")("div"),"a",{get:function(){return 7}}).a})},a481:function(t,e,n){"use strict";var r=n("cb7c"),o=n("4bf8"),i=n("9def"),a=n("4588"),c=n("0390"),s=n("5f1b"),u=Math.max,f=Math.min,l=Math.floor,p=/\$([$&`']|\d\d?|<[^>]*>)/g,d=/\$([$&`']|\d\d?)/g,h=function(t){return void 0===t?t:String(t)};n("214f")("replace",2,function(t,e,n,v){return[function(r,o){var i=t(this),a=void 0==r?void 0:r[e];return void 0!==a?a.call(r,i,o):n.call(String(i),r,o)},function(t,e){var o=v(n,t,this,e);if(o.done)return o.value;var l=r(t),p=String(this),d="function"===typeof e;d||(e=String(e));var g=l.global;if(g){var m=l.unicode;l.lastIndex=0}var b=[];while(1){var _=s(l,p);if(null===_)break;if(b.push(_),!g)break;var w=String(_[0]);""===w&&(l.lastIndex=c(p,i(l.lastIndex),m))}for(var x="",O=0,S=0;S=O&&(x+=p.slice(O,A)+j,O=A+$.length)}return x+p.slice(O)}];function y(t,e,r,i,a,c){var s=r+t.length,u=i.length,f=d;return void 0!==a&&(a=o(a),f=p),n.call(c,f,function(n,o){var c;switch(o.charAt(0)){case"$":return"$";case"&":return t;case"`":return e.slice(0,r);case"'":return e.slice(s);case"<":c=a[o.slice(1,-1)];break;default:var f=+o;if(0===f)return o;if(f>u){var p=l(f/10);return 0===p?o:p<=u?void 0===i[p-1]?o.charAt(1):i[p-1]+o.charAt(1):o}c=i[f-1]}return void 0===c?"":c})}})},a4bb:function(t,e,n){t.exports=n("fda6")},a5ab:function(t,e,n){var r=n("a812"),o=Math.min;t.exports=function(t){return t>0?o(r(t),9007199254740991):0}},a638:function(t,e){t.exports={}},a745:function(t,e,n){t.exports=n("d604")},a7d3:function(t,e){var n=t.exports={version:"2.6.5"};"number"==typeof __e&&(__e=n)},a812:function(t,e){var n=Math.ceil,r=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?r:n)(t)}},a9f2:function(t,e,n){var r=n("9184");t.exports=function(t,e,n){if(r(t),void 0===e)return t;switch(n){case 1:return function(n){return t.call(e,n)};case 2:return function(n,r){return t.call(e,n,r)};case 3:return function(n,r,o){return t.call(e,n,r,o)}}return function(){return t.apply(e,arguments)}}},aa77:function(t,e,n){var r=n("5ca1"),o=n("be13"),i=n("79e5"),a=n("fdef"),c="["+a+"]",s="​…",u=RegExp("^"+c+c+"*"),f=RegExp(c+c+"*$"),l=function(t,e,n){var o={},c=i(function(){return!!a[t]()||s[t]()!=s}),u=o[t]=c?e(p):a[t];n&&(o[n]=u),r(r.P+r.F*c,"String",o)},p=l.trim=function(t,e){return t=String(o(t)),1&e&&(t=t.replace(u,"")),2&e&&(t=t.replace(f,"")),t};t.exports=l},aa82:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=function(t){return(0,r.withParams)({type:"requiredIf",prop:t},function(e,n){return!(0,r.ref)(t,this,n)||(0,r.req)(e)})}},aab6:function(t,e,n){var r=n("e4e1").f,o=n("0f4a"),i=n("b67f")("toStringTag");t.exports=function(t,e,n){t&&!o(t=n?t:t.prototype,i)&&r(t,i,{configurable:!0,value:e})}},aae3:function(t,e,n){var r=n("d3f4"),o=n("2d95"),i=n("2b4c")("match");t.exports=function(t){var e;return r(t)&&(void 0!==(e=t[i])?!!e:"RegExp"==o(t))}},ab4c:function(t,e){t.exports=function(t){return"object"===typeof t?null!==t:"function"===typeof t}},ac6a:function(t,e,n){for(var r=n("cadf"),o=n("0d58"),i=n("2aba"),a=n("7726"),c=n("32e9"),s=n("84f2"),u=n("2b4c"),f=u("iterator"),l=u("toStringTag"),p=s.Array,d={CSSRuleList:!0,CSSStyleDeclaration:!1,CSSValueList:!1,ClientRectList:!1,DOMRectList:!1,DOMStringList:!1,DOMTokenList:!0,DataTransferItemList:!1,FileList:!1,HTMLAllCollection:!1,HTMLCollection:!1,HTMLFormElement:!1,HTMLSelectElement:!1,MediaList:!0,MimeTypeArray:!1,NamedNodeMap:!1,NodeList:!0,PaintRequestList:!1,Plugin:!1,PluginArray:!1,SVGLengthList:!1,SVGNumberList:!1,SVGPathSegList:!1,SVGPointList:!1,SVGStringList:!1,SVGTransformList:!1,SourceBufferList:!1,StyleSheetList:!0,TextTrackCueList:!1,TextTrackList:!1,TouchList:!1},h=o(d),v=0;vf)if(c=s[f++],c!=c)return!0}else for(;u>f;f++)if((t||f in s)&&s[f]===n)return t||f||0;return!t&&-1}}},c5f6:function(t,e,n){"use strict";var r=n("7726"),o=n("69a8"),i=n("2d95"),a=n("5dbc"),c=n("6a99"),s=n("79e5"),u=n("9093").f,f=n("11e9").f,l=n("86cc").f,p=n("aa77").trim,d="Number",h=r[d],v=h,y=h.prototype,g=i(n("2aeb")(y))==d,m="trim"in String.prototype,b=function(t){var e=c(t,!1);if("string"==typeof e&&e.length>2){e=m?e.trim():p(e,3);var n,r,o,i=e.charCodeAt(0);if(43===i||45===i){if(n=e.charCodeAt(2),88===n||120===n)return NaN}else if(48===i){switch(e.charCodeAt(1)){case 66:case 98:r=2,o=49;break;case 79:case 111:r=8,o=55;break;default:return+e}for(var a,s=e.slice(2),u=0,f=s.length;uo)return NaN;return parseInt(s,r)}}return+e};if(!h(" 0o1")||!h("0b1")||h("+0x1")){h=function(t){var e=arguments.length<1?0:t,n=this;return n instanceof h&&(g?s(function(){y.valueOf.call(n)}):i(n)!=d)?a(new v(b(e)),n,h):b(e)};for(var _,w=n("9e1e")?u(v):"MAX_VALUE,MIN_VALUE,NaN,NEGATIVE_INFINITY,POSITIVE_INFINITY,EPSILON,isFinite,isInteger,isNaN,isSafeInteger,MAX_SAFE_INTEGER,MIN_SAFE_INTEGER,parseFloat,parseInt,isInteger".split(","),x=0;w.length>x;x++)o(v,_=w[x])&&!o(h,_)&&l(h,_,f(v,_));h.prototype=y,y.constructor=h,n("2aba")(r,d,h)}},c609:function(t,e,n){"use strict";var r=n("d13f"),o=n("03ca"),i=n("75c9");r(r.S,"Promise",{try:function(t){var e=o.f(this),n=i(t);return(n.e?e.reject:e.resolve)(n.v),e.promise}})},c69a:function(t,e,n){t.exports=!n("9e1e")&&!n("79e5")(function(){return 7!=Object.defineProperty(n("230e")("div"),"a",{get:function(){return 7}}).a})},c764:function(t,e,n){n("dc9b"),t.exports=n("a7d3").Object.values},c8ba:function(t,e){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(r){"object"===typeof window&&(n=window)}t.exports=n},c8bb:function(t,e,n){t.exports=n("89ca")},c99d:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=(0,r.withParams)({type:"ipAddress"},function(t){if(!(0,r.req)(t))return!0;if("string"!==typeof t)return!1;var e=t.split(".");return 4===e.length&&e.every(o)});var o=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}},ca38:function(t,e){var n=t.exports={version:"2.6.5"};"number"==typeof __e&&(__e=n)},ca5a:function(t,e){var n=0,r=Math.random();t.exports=function(t){return"Symbol(".concat(void 0===t?"":t,")_",(++n+r).toString(36))}},cadf:function(t,e,n){"use strict";var r=n("9c6c"),o=n("d53b"),i=n("84f2"),a=n("6821");t.exports=n("01f9")(Array,"Array",function(t,e){this._t=a(t),this._i=0,this._k=e},function(){var t=this._t,e=this._k,n=this._i++;return!t||n>=t.length?(this._t=void 0,o(1)):o(0,"keys"==e?n:"values"==e?t[n]:[n,t[n]])},"values"),i.Arguments=i.Array,r("keys"),r("values"),r("entries")},cb7c:function(t,e,n){var r=n("d3f4");t.exports=function(t){if(!r(t))throw TypeError(t+" is not an object!");return t}},cc1d:function(t,e,n){var r=n("58b9"),o=n("04cf"),i=n("4052");t.exports=function(t){return function(e,n,a){var c,s=r(e),u=o(s.length),f=i(a,u);if(t&&n!=n){while(u>f)if(c=s[f++],c!=c)return!0}else for(;u>f;f++)if((t||f in s)&&s[f]===n)return t||f||0;return!t&&-1}}},cc20:function(t,e,n){"use strict";var r=n("9184");function o(t){var e,n;this.promise=new t(function(t,r){if(void 0!==e||void 0!==n)throw TypeError("Bad Promise constructor");e=t,n=r}),this.resolve=r(e),this.reject=r(n)}t.exports.f=function(t){return new o(t)}},cd1c:function(t,e,n){var r=n("e853");t.exports=function(t,e){return new(r(t))(e)}},ce10:function(t,e,n){var r=n("69a8"),o=n("6821"),i=n("c366")(!1),a=n("613b")("IE_PROTO");t.exports=function(t,e){var n,c=o(t),s=0,u=[];for(n in c)n!=a&&r(c,n)&&u.push(n);while(e.length>s)r(c,n=e[s++])&&(~i(u,n)||u.push(n));return u}},cebc:function(t,e,n){"use strict";var r=n("268f"),o=n.n(r),i=n("e265"),a=n.n(i),c=n("a4bb"),s=n.n(c),u=n("85f2"),f=n.n(u);function l(t,e,n){return e in t?f()(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function p(t){for(var e=1;eu)i.call(a,n=c[u++])&&f.push(t?[n,a[n]]:a[n]);return f}}},d127:function(t,e,n){n("0a0a")("asyncIterator")},d13f:function(t,e,n){var r=n("da3c"),o=n("a7d3"),i=n("bc25"),a=n("8ce0"),c=n("43c8"),s="prototype",u=function(t,e,n){var f,l,p,d=t&u.F,h=t&u.G,v=t&u.S,y=t&u.P,g=t&u.B,m=t&u.W,b=h?o:o[e]||(o[e]={}),_=b[s],w=h?r:v?r[e]:(r[e]||{})[s];for(f in h&&(n=e),n)l=!d&&w&&void 0!==w[f],l&&c(b,f)||(p=l?w[f]:n[f],b[f]=h&&"function"!=typeof w[f]?n[f]:g&&l?i(p,r):m&&w[f]==p?function(t){var e=function(e,n,r){if(this instanceof t){switch(arguments.length){case 0:return new t;case 1:return new t(e);case 2:return new t(e,n)}return new t(e,n,r)}return t.apply(this,arguments)};return e[s]=t[s],e}(p):y&&"function"==typeof p?i(Function.call,p):p,y&&((b.virtual||(b.virtual={}))[f]=p,t&u.R&&_&&!_[f]&&a(_,f,p)))};u.F=1,u.G=2,u.S=4,u.P=8,u.B=16,u.W=32,u.U=64,u.R=128,t.exports=u},d24f:function(t,e,n){n("0a0a")("observable")},d256:function(t,e,n){"use strict";var r=n("da3c"),o=n("43c8"),i=n("7d95"),a=n("d13f"),c=n("2312"),s=n("6277").KEY,u=n("d782"),f=n("7772"),l=n("c0d8"),p=n("7b00"),d=n("1b55"),h=n("fda1"),v=n("0a0a"),y=n("d2d6"),g=n("b5aa"),m=n("0f89"),b=n("6f8a"),_=n("6a9b"),w=n("2ea1"),x=n("f845"),O=n("7108"),S=n("565d"),$=n("626e"),A=n("3adc"),C=n("7633"),k=$.f,E=A.f,M=S.f,j=r.Symbol,T=r.JSON,P=T&&T.stringify,D="prototype",L=d("_hidden"),I=d("toPrimitive"),N={}.propertyIsEnumerable,R=f("symbol-registry"),F=f("symbols"),H=f("op-symbols"),z=Object[D],V="function"==typeof j,B=r.QObject,U=!B||!B[D]||!B[D].findChild,Y=i&&u(function(){return 7!=O(E({},"a",{get:function(){return E(this,"a",{value:7}).a}})).a})?function(t,e,n){var r=k(z,e);r&&delete z[e],E(t,e,n),r&&t!==z&&E(z,e,r)}:E,q=function(t){var e=F[t]=O(j[D]);return e._k=t,e},W=V&&"symbol"==typeof j.iterator?function(t){return"symbol"==typeof t}:function(t){return t instanceof j},G=function(t,e,n){return t===z&&G(H,e,n),m(t),e=w(e,!0),m(n),o(F,e)?(n.enumerable?(o(t,L)&&t[L][e]&&(t[L][e]=!1),n=O(n,{enumerable:x(0,!1)})):(o(t,L)||E(t,L,x(1,{})),t[L][e]=!0),Y(t,e,n)):E(t,e,n)},K=function(t,e){m(t);var n,r=y(e=_(e)),o=0,i=r.length;while(i>o)G(t,n=r[o++],e[n]);return t},X=function(t,e){return void 0===e?O(t):K(O(t),e)},J=function(t){var e=N.call(this,t=w(t,!0));return!(this===z&&o(F,t)&&!o(H,t))&&(!(e||!o(this,t)||!o(F,t)||o(this,L)&&this[L][t])||e)},Z=function(t,e){if(t=_(t),e=w(e,!0),t!==z||!o(F,e)||o(H,e)){var n=k(t,e);return!n||!o(F,e)||o(t,L)&&t[L][e]||(n.enumerable=!0),n}},Q=function(t){var e,n=M(_(t)),r=[],i=0;while(n.length>i)o(F,e=n[i++])||e==L||e==s||r.push(e);return r},tt=function(t){var e,n=t===z,r=M(n?H:_(t)),i=[],a=0;while(r.length>a)!o(F,e=r[a++])||n&&!o(z,e)||i.push(F[e]);return i};V||(j=function(){if(this instanceof j)throw TypeError("Symbol is not a constructor!");var t=p(arguments.length>0?arguments[0]:void 0),e=function(n){this===z&&e.call(H,n),o(this,L)&&o(this[L],t)&&(this[L][t]=!1),Y(this,t,x(1,n))};return i&&U&&Y(z,t,{configurable:!0,set:e}),q(t)},c(j[D],"toString",function(){return this._k}),$.f=Z,A.f=G,n("d876").f=S.f=Q,n("d74e").f=J,n("31c2").f=tt,i&&!n("b457")&&c(z,"propertyIsEnumerable",J,!0),h.f=function(t){return q(d(t))}),a(a.G+a.W+a.F*!V,{Symbol:j});for(var et="hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables".split(","),nt=0;et.length>nt;)d(et[nt++]);for(var rt=C(d.store),ot=0;rt.length>ot;)v(rt[ot++]);a(a.S+a.F*!V,"Symbol",{for:function(t){return o(R,t+="")?R[t]:R[t]=j(t)},keyFor:function(t){if(!W(t))throw TypeError(t+" is not a symbol!");for(var e in R)if(R[e]===t)return e},useSetter:function(){U=!0},useSimple:function(){U=!1}}),a(a.S+a.F*!V,"Object",{create:X,defineProperty:G,defineProperties:K,getOwnPropertyDescriptor:Z,getOwnPropertyNames:Q,getOwnPropertySymbols:tt}),T&&a(a.S+a.F*(!V||u(function(){var t=j();return"[null]"!=P([t])||"{}"!=P({a:t})||"{}"!=P(Object(t))})),"JSON",{stringify:function(t){var e,n,r=[t],o=1;while(arguments.length>o)r.push(arguments[o++]);if(n=e=r[1],(b(e)||void 0!==t)&&!W(t))return g(e)||(e=function(t,e){if("function"==typeof n&&(e=n.call(this,t,e)),!W(e))return e}),r[1]=e,P.apply(T,r)}}),j[D][I]||n("8ce0")(j[D],I,j[D].valueOf),l(j,"Symbol"),l(Math,"Math",!0),l(r.JSON,"JSON",!0)},d294:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=function(){for(var t=arguments.length,e=Array(t),n=0;n0&&e.reduce(function(e,n){return e||n.apply(t,r)},!1)})}},d2c8:function(t,e,n){var r=n("aae3"),o=n("be13");t.exports=function(t,e,n){if(r(e))throw TypeError("String#"+n+" doesn't accept regex!");return String(o(t))}},d2d6:function(t,e,n){var r=n("7633"),o=n("31c2"),i=n("d74e");t.exports=function(t){var e=r(t),n=o.f;if(n){var a,c=n(t),s=i.f,u=0;while(c.length>u)s.call(t,a=c[u++])&&e.push(a)}return e}},d38f:function(t,e,n){var r=n("7d8a"),o=n("1b55")("iterator"),i=n("b22a");t.exports=n("a7d3").isIterable=function(t){var e=Object(t);return void 0!==e[o]||"@@iterator"in e||i.hasOwnProperty(r(e))}},d3f4:function(t,e){t.exports=function(t){return"object"===typeof t?null!==t:"function"===typeof t}},d43f:function(t,e,n){var r=n("71fa");t.exports=Object("z").propertyIsEnumerable(0)?Object:function(t){return"String"==r(t)?t.split(""):Object(t)}},d4f4:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=(0,r.withParams)({type:"required"},r.req)},d53b:function(t,e){t.exports=function(t,e){return{value:e,done:!!t}}},d604:function(t,e,n){n("1938"),t.exports=n("a7d3").Array.isArray},d74e:function(t,e){e.f={}.propertyIsEnumerable},d782:function(t,e){t.exports=function(t){try{return!!t()}catch(e){return!0}}},d876:function(t,e,n){var r=n("2695"),o=n("0029").concat("length","prototype");e.f=Object.getOwnPropertyNames||function(t){return r(t,o)}},d8db:function(t,e,n){var r=n("b808").document;t.exports=r&&r.documentElement},d8e8:function(t,e){t.exports=function(t){if("function"!=typeof t)throw TypeError(t+" is not a function!");return t}},d9fc:function(t,e,n){"use strict";var r=n("569f"),o=n("ca38"),i=n("b808"),a=n("1aa7"),c=n("1c08");r(r.P+r.R,"Promise",{finally:function(t){var e=a(this,o.Promise||i.Promise),n="function"==typeof t;return this.then(n?function(n){return c(e,t()).then(function(){return n})}:t,n?function(n){return c(e,t()).then(function(){throw n})}:t)}})},da3c:function(t,e){var n=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},db0c:function(t,e,n){t.exports=n("c764")},dc9b:function(t,e,n){var r=n("d13f"),o=n("cff3")(!1);r(r.S,"Object",{values:function(t){return o(t)}})},dcc3:function(t,e){t.exports=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}}},dd04:function(t,e,n){n("12fd9"),n("93c4"),n("b42c"),n("5b5f"),n("b604"),n("c609"),t.exports=n("a7d3").Promise},decf:function(t,e,n){var r=n("0f89"),o=n("6f8a"),i=n("03ca");t.exports=function(t,e){if(r(t),o(e)&&e.constructor===t)return e;var n=i.f(t),a=n.resolve;return a(e),n.promise}},df0a:function(t,e,n){var r,o,i,a=n("bc25"),c=n("196c"),s=n("103a"),u=n("12fd"),f=n("da3c"),l=f.process,p=f.setImmediate,d=f.clearImmediate,h=f.MessageChannel,v=f.Dispatch,y=0,g={},m="onreadystatechange",b=function(){var t=+this;if(g.hasOwnProperty(t)){var e=g[t];delete g[t],e()}},_=function(t){b.call(t.data)};p&&d||(p=function(t){var e=[],n=1;while(arguments.length>n)e.push(arguments[n++]);return g[++y]=function(){c("function"==typeof t?t:Function(t),e)},r(y),y},d=function(t){delete g[t]},"process"==n("6e1f")(l)?r=function(t){l.nextTick(a(b,t,1))}:v&&v.now?r=function(t){v.now(a(b,t,1))}:h?(o=new h,i=o.port2,o.port1.onmessage=_,r=a(i.postMessage,i,1)):f.addEventListener&&"function"==typeof postMessage&&!f.importScripts?(r=function(t){f.postMessage(t+"","*")},f.addEventListener("message",_,!1)):r=m in u("script")?function(t){s.appendChild(u("script"))[m]=function(){s.removeChild(this),b.call(t)}}:function(t){setTimeout(a(b,t,1),0)}),t.exports={set:p,clear:d}},e11e:function(t,e){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},e265:function(t,e,n){t.exports=n("5698")},e341:function(t,e,n){var r=n("d13f");r(r.S+r.F*!n("7d95"),"Object",{defineProperty:n("3adc").f})},e4a9:function(t,e,n){"use strict";var r=n("b457"),o=n("d13f"),i=n("2312"),a=n("8ce0"),c=n("b22a"),s=n("5ce7"),u=n("c0d8"),f=n("ff0c"),l=n("1b55")("iterator"),p=!([].keys&&"next"in[].keys()),d="@@iterator",h="keys",v="values",y=function(){return this};t.exports=function(t,e,n,g,m,b,_){s(n,e,g);var w,x,O,S=function(t){if(!p&&t in k)return k[t];switch(t){case h:return function(){return new n(this,t)};case v:return function(){return new n(this,t)}}return function(){return new n(this,t)}},$=e+" Iterator",A=m==v,C=!1,k=t.prototype,E=k[l]||k[d]||m&&k[m],M=E||S(m),j=m?A?S("entries"):M:void 0,T="Array"==e&&k.entries||E;if(T&&(O=f(T.call(new t)),O!==Object.prototype&&O.next&&(u(O,$,!0),r||"function"==typeof O[l]||a(O,l,y))),A&&E&&E.name!==v&&(C=!0,M=function(){return E.call(this)}),r&&!_||!p&&!C&&k[l]||a(k,l,M),c[e]=M,c[$]=y,m)if(w={values:A?M:S(v),keys:b?M:S(h),entries:j},_)for(x in w)x in k||i(k,x,w[x]);else o(o.P+o.F*(p||C),e,w);return w}},e4e1:function(t,e,n){var r=n("27b2"),o=n("e830"),i=n("b938"),a=Object.defineProperty;e.f=n("45e2")?Object.defineProperty:function(t,e,n){if(r(t),e=i(e,!0),r(n),o)try{return a(t,e,n)}catch(c){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(t[e]=n.value),t}},e523:function(t,e,n){var r=n("e4e1"),o=n("27b2"),i=n("9ed1");t.exports=n("45e2")?Object.defineProperties:function(t,e){o(t);var n,a=i(e),c=a.length,s=0;while(c>s)r.f(t,n=a[s++],e[n]);return t}},e5fa:function(t,e){t.exports=function(t){if(void 0==t)throw TypeError("Can't call method on "+t);return t}},e652:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=function(t){return(0,r.withParams)({type:"requiredUnless",prop:t},function(e,n){return!!(0,r.ref)(t,this,n)||(0,r.req)(e)})}},e6a1:function(t,e){t.exports=!1},e814:function(t,e,n){t.exports=n("54aa")},e830:function(t,e,n){t.exports=!n("45e2")&&!n("b629")(function(){return 7!=Object.defineProperty(n("781f")("div"),"a",{get:function(){return 7}}).a})},e853:function(t,e,n){var r=n("d3f4"),o=n("1169"),i=n("2b4c")("species");t.exports=function(t){var e;return o(t)&&(e=t.constructor,"function"!=typeof e||e!==Array&&!o(e.prototype)||(e=void 0),r(e)&&(e=e[i],null===e&&(e=void 0))),void 0===e?Array:e}},eb66:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=function(t){return(0,r.withParams)({type:"minValue",min:t},function(e){return!(0,r.req)(e)||(!/\s/.test(e)||e instanceof Date)&&+e>=+t})}},ebd6:function(t,e,n){var r=n("cb7c"),o=n("d8e8"),i=n("2b4c")("species");t.exports=function(t,e){var n,a=r(t).constructor;return void 0===a||void 0==(n=r(a)[i])?e:o(n)}},ec11:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=function(t,e){return(0,r.withParams)({type:"between",min:t,max:e},function(n){return!(0,r.req)(n)||(!/\s/.test(n)||n instanceof Date)&&+t<=+n&&+e>=+n})}},ec5b:function(t,e,n){n("e341");var r=n("a7d3").Object;t.exports=function(t,e,n){return r.defineProperty(t,e,n)}},ed0f:function(t,e,n){var r=n("27b2"),o=n("e523"),i=n("49c1"),a=n("be5a")("IE_PROTO"),c=function(){},s="prototype",u=function(){var t,e=n("781f")("iframe"),r=i.length,o="<",a=">";e.style.display="none",n("d8db").appendChild(e),e.src="javascript:",t=e.contentWindow.document,t.open(),t.write(o+"script"+a+"document.F=Object"+o+"/script"+a),t.close(),u=t.F;while(r--)delete u[s][i[r]];return u()};t.exports=Object.create||function(t,e){var n;return null!==t?(c[s]=r(t),n=new c,c[s]=null,n[a]=t):n=u(),void 0===e?n:o(n,e)}},eec7:function(t,e,n){"use strict";var r=n("ed0f"),o=n("dcc3"),i=n("aab6"),a={};n("a0a8")(a,n("b67f")("iterator"),function(){return this}),t.exports=function(t,e,n){t.prototype=r(a,{next:o(1,n)}),i(t,e+" Iterator")}},ef26:function(t,e){t.exports=function(t,e,n){var r=void 0===n;switch(e.length){case 0:return r?t():t.call(n);case 1:return r?t(e[0]):t.call(n,e[0]);case 2:return r?t(e[0],e[1]):t.call(n,e[0],e[1]);case 3:return r?t(e[0],e[1],e[2]):t.call(n,e[0],e[1],e[2]);case 4:return r?t(e[0],e[1],e[2],e[3]):t.call(n,e[0],e[1],e[2],e[3])}return t.apply(n,e)}},f159:function(t,e,n){var r=n("7d8a"),o=n("1b55")("iterator"),i=n("b22a");t.exports=n("a7d3").getIteratorMethod=function(t){if(void 0!=t)return t[o]||t["@@iterator"]||i[r(t)]}},f26d:function(t,e,n){var r=n("a638"),o=n("b67f")("iterator"),i=Array.prototype;t.exports=function(t){return void 0!==t&&(r.Array===t||i[o]===t)}},f2f3:function(t,e,n){"use strict";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},r(t)}var o={namespaced:!0,state:{locale:null,fallback:null,translations:{}},mutations:{SET_LOCALE:function(t,e){t.locale=e.locale},ADD_LOCALE:function(t,e){var n=i(e.translations);if(t.translations.hasOwnProperty(e.locale)){var r=t.translations[e.locale];t.translations[e.locale]=Object.assign({},r,n)}else t.translations[e.locale]=n;try{t.translations.__ob__&&t.translations.__ob__.dep.notify()}catch(o){}},REPLACE_LOCALE:function(t,e){var n=i(e.translations);t.translations[e.locale]=n;try{t.translations.__ob__&&t.translations.__ob__.dep.notify()}catch(r){}},REMOVE_LOCALE:function(t,e){if(t.translations.hasOwnProperty(e.locale)){t.locale===e.locale&&(t.locale=null);var n=Object.assign({},t.translations);delete n[e.locale],t.translations=n}},SET_FALLBACK_LOCALE:function(t,e){t.fallback=e.locale}},actions:{setLocale:function(t,e){t.commit({type:"SET_LOCALE",locale:e.locale})},addLocale:function(t,e){t.commit({type:"ADD_LOCALE",locale:e.locale,translations:e.translations})},replaceLocale:function(t,e){t.commit({type:"REPLACE_LOCALE",locale:e.locale,translations:e.translations})},removeLocale:function(t,e){t.commit({type:"REMOVE_LOCALE",locale:e.locale,translations:e.translations})},setFallbackLocale:function(t,e){t.commit({type:"SET_FALLBACK_LOCALE",locale:e.locale})}}},i=function t(e){var n={};for(var o in e)if(e.hasOwnProperty(o)){var i=r(e[o]);if(a(e[o])){for(var c=e[o].length,s=0;s1?1:0;case"lv":return e%10===1&&e%100!==11?0:0!==e?1:2;case"lt":return e%10===1&&e%100!==11?0:e%10>=2&&(e%100<10||e%100>=20)?1:2;case"be":case"bs":case"hr":case"ru":case"sr":case"uk":return e%10===1&&e%100!==11?0:e%10>=2&&e%10<=4&&(e%100<10||e%100>=20)?1:2;case"mnk":return 0===e?0:1===e?1:2;case"ro":return 1===e?0:0===e||e%100>0&&e%100<20?1:2;case"pl":return 1===e?0:e%10>=2&&e%10<=4&&(e%100<10||e%100>=20)?1:2;case"cs":case"sk":return 1===e?0:e>=2&&e<=4?1:2;case"csb":return 1===e?0:e%10>=2&&e%10<=4&&(e%100<10||e%100>=20)?1:2;case"sl":return e%100===1?0:e%100===2?1:e%100===3||e%100===4?2:3;case"mt":return 1===e?0:0===e||e%100>1&&e%100<11?1:e%100>10&&e%100<20?2:3;case"gd":return 1===e||11===e?0:2===e||12===e?1:e>2&&e<20?2:3;case"cy":return 1===e?0:2===e?1:8!==e&&11!==e?2:3;case"kw":return 1===e?0:2===e?1:3===e?2:3;case"ga":return 1===e?0:2===e?1:e>2&&e<7?2:e>6&&e<11?3:4;case"ar":return 0===e?0:1===e?1:2===e?2:e%100>=3&&e%100<=10?3:e%100>=11?4:5;default:return 1!==e?1:0}}},s={install:function(t,e,n){"string"!==typeof arguments[2]&&"string"!==typeof arguments[3]||(console.warn("i18n: Registering the plugin vuex-i18n with a string for `moduleName` or `identifiers` is deprecated. Use a configuration object instead.","https://github.com/dkfbasel/vuex-i18n#setup"),n={moduleName:arguments[2],identifiers:arguments[3]}),n=Object.assign({warnings:!0,moduleName:"i18n",identifiers:["{","}"],preserveState:!1,translateFilterName:"translate",translateInFilterName:"translateIn",onTranslationNotFound:function(){}},n);var r=n.moduleName,i=n.identifiers,a=n.translateFilterName,c=n.translateInFilterName,s=n.onTranslationNotFound;if("function"!==typeof s&&(console.error("i18n: i18n config option onTranslationNotFound must be a function"),s=function(){}),e.registerModule(r,o,{preserveState:n.preserveState}),!1===e.state.hasOwnProperty(r))return console.error("i18n: i18n vuex module is not correctly initialized. Please check the module name:",r),t.prototype.$i18n=function(t){return t},t.prototype.$getLanguage=function(){return null},void(t.prototype.$setLanguage=function(){console.error("i18n: i18n vuex module is not correctly initialized")});var f=u(i,n.warnings),l=function(){var t=e.state[r].locale;return p.apply(void 0,[t].concat(Array.prototype.slice.call(arguments)))},p=function(t){var o=arguments,i="",a="",c={},u=null,l=o.length;if(l>=3&&"string"===typeof o[2]?(i=o[1],a=o[2],l>3&&(c=o[3]),l>4&&(u=o[4])):(i=o[1],a=i,l>2&&(c=o[2]),l>3&&(u=o[3])),!t)return n.warnings&&console.warn("i18n: i18n locale is not set when trying to access translations:",i),a;var p=e.state[r].translations,d=e.state[r].fallback,h=t.split("-"),v=!0;if(!1===p.hasOwnProperty(t)?v=!1:!1===p[t].hasOwnProperty(i)&&(v=!1),!0===v)return f(t,p[t][i],c,u);if(h.length>1&&!0===p.hasOwnProperty(h[0])&&!0===p[h[0]].hasOwnProperty(i))return f(h[0],p[h[0]][i],c,u);var y=s(t,i,a);return y&&Promise.resolve(y).then(function(e){var n={};n[i]=e,b(t,n)}),!1===p.hasOwnProperty(d)?f(t,a,c,u):!1===p[d].hasOwnProperty(i)?f(d,a,c,u):f(t,p[d][i],c,u)},d=function(t,e){for(var n=arguments.length,r=new Array(n>2?n-2:0),o=2;o1&&void 0!==arguments[1]?arguments[1]:"fallback",o=e.state[r].locale,i=e.state[r].fallback,a=e.state[r].translations;if(a.hasOwnProperty(o)&&a[o].hasOwnProperty(t))return!0;if("strict"==n)return!1;var c=o.split("-");return!!(c.length>1&&a.hasOwnProperty(c[0])&&a[c[0]].hasOwnProperty(t))||"locale"!=n&&!(!a.hasOwnProperty(i)||!a[i].hasOwnProperty(t))},v=function(t){e.dispatch({type:"".concat(r,"/setFallbackLocale"),locale:t})},y=function(t){e.dispatch({type:"".concat(r,"/setLocale"),locale:t})},g=function(){return e.state[r].locale},m=function(){return Object.keys(e.state[r].translations)},b=function(t,n){return e.dispatch({type:"".concat(r,"/addLocale"),locale:t,translations:n})},_=function(t,n){return e.dispatch({type:"".concat(r,"/replaceLocale"),locale:t,translations:n})},w=function(t){e.state[r].translations.hasOwnProperty(t)&&e.dispatch({type:"".concat(r,"/removeLocale"),locale:t})},x=function(t){return n.warnings&&console.warn("i18n: $i18n.exists is depreceated. Please use $i18n.localeExists instead. It provides exactly the same functionality."),O(t)},O=function(t){return e.state[r].translations.hasOwnProperty(t)};t.prototype.$i18n={locale:g,locales:m,set:y,add:b,replace:_,remove:w,fallback:v,localeExists:O,keyExists:h,translate:l,translateIn:p,exists:x},t.i18n={locale:g,locales:m,set:y,add:b,replace:_,remove:w,fallback:v,translate:l,translateIn:p,localeExists:O,keyExists:h,exists:x},t.prototype.$t=l,t.prototype.$tlang=p,t.filter(a,l),t.filter(c,d)}},u=function(t){var e=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];null!=t&&2==t.length||console.warn("i18n: You must specify the start and end character identifying variable substitutions");var n=new RegExp(t[0]+"{1}(\\w{1}|\\w.+?)"+t[1]+"{1}","g"),o=function(r,o){return r.replace?r.replace(n,function(n){var i=n.replace(t[0],"").replace(t[1],"");return void 0!==o[i]?o[i]:(e&&(console.group?console.group("i18n: Not all placeholders found"):console.warn("i18n: Not all placeholders found"),console.warn("Text:",r),console.warn("Placeholder:",n),console.groupEnd&&console.groupEnd()),n)}):r},i=function(t,n){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},a=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null,s=r(n),u=r(a),l=function(){return f(n)?n.map(function(t){return o(t,i,!1)}):"string"===s?o(n,i,!0):void 0};if(null===a)return l();if("number"!==u)return e&&console.warn("i18n: pluralization is not a number"),l();var p=l(),d=null;d=f(p)&&p.length>0?p:p.split(":::");var h=c.getTranslationIndex(t,a);return"undefined"===typeof d[h]?(e&&console.warn("i18n: pluralization not provided in locale",n,t,h),d[0].trim()):d[h].trim()};return i};function f(t){return!!t&&Array===t.constructor}var l={store:o,plugin:s};e["a"]=l},f2fe:function(t,e){t.exports=function(t){if("function"!=typeof t)throw TypeError(t+" is not a function!");return t}},f3e0:function(t,e,n){var r=n("0185"),o=n("7633");n("c165")("keys",function(){return function(t){return o(r(t))}})},f499:function(t,e,n){t.exports=n("1c01")},f559:function(t,e,n){"use strict";var r=n("5ca1"),o=n("9def"),i=n("d2c8"),a="startsWith",c=""[a];r(r.P+r.F*n("5147")(a),"String",{startsWith:function(t){var e=i(this,t,a),n=o(Math.min(arguments.length>1?arguments[1]:void 0,e.length)),r=String(t);return c?c.call(e,r,n):e.slice(n,n+r.length)===r}})},f568:function(t,e,n){var r=n("3adc"),o=n("0f89"),i=n("7633");t.exports=n("7d95")?Object.defineProperties:function(t,e){o(t);var n,a=i(e),c=a.length,s=0;while(c>s)r.f(t,n=a[s++],e[n]);return t}},f6d7:function(t,e,n){var r=n("0f4a"),o=n("58b9"),i=n("cc1d")(!1),a=n("be5a")("IE_PROTO");t.exports=function(t,e){var n,c=o(t),s=0,u=[];for(n in c)n!=a&&r(c,n)&&u.push(n);while(e.length>s)r(c,n=e[s++])&&(~i(u,n)||u.push(n));return u}},f845:function(t,e){t.exports=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}}},f906:function(t,e,n){!function(e,n){t.exports=n()}(0,function(){"use strict";var t,e=/(\[[^[]*\])|([-:\/.()\s]+)|(A|a|YYYY|YY?|MM?M?M?|Do|DD?|hh?|HH?|mm?|ss?|S{1,3}|z|ZZ?)/g,n=/\d\d/,r=/\d\d?/,o=/\d*[^\s\d-:\/.()]+/,i=function(t){return function(e){this[t]=+e}},a=[/[+-]\d\d:?\d\d/,function(t){var e,n;(this.zone||(this.zone={})).offset=(e=t.match(/([+-]|\d\d)/g),0===(n=60*e[1]+ +e[2])?0:"+"===e[0]?-n:n)}],c={A:[/[AP]M/,function(t){this.afternoon="PM"===t}],a:[/[ap]m/,function(t){this.afternoon="pm"===t}],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,i("seconds")],ss:[r,i("seconds")],m:[r,i("minutes")],mm:[r,i("minutes")],H:[r,i("hours")],h:[r,i("hours")],HH:[r,i("hours")],hh:[r,i("hours")],D:[r,i("day")],DD:[n,i("day")],Do:[o,function(e){var n=t.ordinal,r=e.match(/\d+/);if(this.day=r[0],n)for(var o=1;o<=31;o+=1)n(o).replace(/\[|\]/g,"")===e&&(this.day=o)}],M:[r,i("month")],MM:[n,i("month")],MMM:[o,function(e){var n=t,r=n.months,o=n.monthsShort,i=o?o.findIndex(function(t){return t===e}):r.findIndex(function(t){return t.substr(0,3)===e});if(i<0)throw new Error;this.month=i+1}],MMMM:[o,function(e){var n=t.months.indexOf(e);if(n<0)throw new Error;this.month=n+1}],Y:[/[+-]?\d+/,i("year")],YY:[n,function(t){t=+t,this.year=t+(t>68?1900:2e3)}],YYYY:[/\d{4}/,i("year")],Z:a,ZZ:a},s=function(t,n,r){try{var o=function(t){for(var n=t.match(e),r=n.length,o=0;o0?a-1:h.getMonth(),g=s||h.getDate(),m=u||0,b=f||0,_=l||0,w=p||0;return r?new Date(Date.UTC(v,y,g,m,b,_,w)):new Date(v,y,g,m,b,_,w)}catch(t){return new Date("")}};return function(e,n,r){var o=n.prototype,i=o.parse;o.parse=function(e){var n=e.date,o=e.format,a=e.pl,c=e.utc;this.$u=c,o?(t=a?r.Ls[a]:this.$locale(),this.$d=s(n,o,c),this.init(e)):i.call(this,e)}}})},fa2d:function(t,e,n){"use strict";var r=n("e6a1"),o=n("569f"),i=n("1f51"),a=n("a0a8"),c=n("a638"),s=n("eec7"),u=n("aab6"),f=n("c339"),l=n("b67f")("iterator"),p=!([].keys&&"next"in[].keys()),d="@@iterator",h="keys",v="values",y=function(){return this};t.exports=function(t,e,n,g,m,b,_){s(n,e,g);var w,x,O,S=function(t){if(!p&&t in k)return k[t];switch(t){case h:return function(){return new n(this,t)};case v:return function(){return new n(this,t)}}return function(){return new n(this,t)}},$=e+" Iterator",A=m==v,C=!1,k=t.prototype,E=k[l]||k[d]||m&&k[m],M=E||S(m),j=m?A?S("entries"):M:void 0,T="Array"==e&&k.entries||E;if(T&&(O=f(T.call(new t)),O!==Object.prototype&&O.next&&(u(O,$,!0),r||"function"==typeof O[l]||a(O,l,y))),A&&E&&E.name!==v&&(C=!0,M=function(){return E.call(this)}),r&&!_||!p&&!C&&k[l]||a(k,l,M),c[e]=M,c[$]=y,m)if(w={values:A?M:S(v),keys:b?M:S(h),entries:j},_)for(x in w)x in k||i(k,x,w[x]);else o(o.P+o.F*(p||C),e,w);return w}},fa54:function(t,e,n){"use strict";var r=n("b3e7"),o=n("245b"),i=n("b22a"),a=n("6a9b");t.exports=n("e4a9")(Array,"Array",function(t,e){this._t=a(t),this._i=0,this._k=e},function(){var t=this._t,e=this._k,n=this._i++;return!t||n>=t.length?(this._t=void 0,o(1)):o(0,"keys"==e?n:"values"==e?t[n]:[n,t[n]])},"values"),i.Arguments=i.Array,r("keys"),r("values"),r("entries")},fab2:function(t,e,n){var r=n("7726").document;t.exports=r&&r.documentElement},fbf4:function(t,e,n){"use strict";function r(t){return null===t||void 0===t}function o(t){return null!==t&&void 0!==t}function i(t,e){return e.tag===t.tag&&e.key===t.key}function a(t){var e=t.tag;t.vm=new e({data:t.args})}function c(t){for(var e=Object.keys(t.args),n=0;nu?f(e,c,v):c>v&&l(t,n,u)}function f(t,e,n){for(;e<=n;++e)a(t[e])}function l(t,e,n){for(;e<=n;++e){var r=t[e];o(r)&&(r.vm.$destroy(),r.vm=null)}}function p(t,e){t!==e&&(e.vm=t.vm,c(e))}function d(t,e){o(t)&&o(e)?t!==e&&u(t,e):o(e)?f(e,0,e.length-1):o(t)&&l(t,0,t.length-1)}function h(t,e,n){return{tag:t,key:e,args:n}}Object.defineProperty(e,"__esModule",{value:!0}),e.patchChildren=d,e.h=h},fd6f:function(t,e,n){var r=n("d13f"),o=n("cff3")(!0);r(r.S,"Object",{entries:function(t){return o(t)}})},fda1:function(t,e,n){e.f=n("1b55")},fda6:function(t,e,n){n("f3e0"),t.exports=n("a7d3").Object.keys},fdef:function(t,e){t.exports="\t\n\v\f\r   ᠎              \u2028\u2029\ufeff"},ff0c:function(t,e,n){var r=n("43c8"),o=n("0185"),i=n("5d8f")("IE_PROTO"),a=Object.prototype;t.exports=Object.getPrototypeOf||function(t){return t=o(t),r(t,i)?t[i]:"function"==typeof t.constructor&&t instanceof t.constructor?t.constructor.prototype:t instanceof Object?a:null}}}]); \ No newline at end of file diff --git a/kirby/panel/jest.config.js b/kirby/panel/jest.config.js new file mode 100755 index 0000000..29fee32 --- /dev/null +++ b/kirby/panel/jest.config.js @@ -0,0 +1,23 @@ +module.exports = { + moduleFileExtensions: [ + 'js', + 'jsx', + 'json', + 'vue' + ], + transform: { + '^.+\\.vue$': 'vue-jest', + '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', + '^.+\\.jsx?$': 'babel-jest' + }, + moduleNameMapper: { + '^@/(.*)$': '/src/$1' + }, + snapshotSerializers: [ + 'jest-serializer-vue' + ], + testMatch: [ + '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' + ], + testURL: 'http://localhost/' +} diff --git a/kirby/phpstan.neon.dist b/kirby/phpstan.neon.dist new file mode 100755 index 0000000..152ea5e --- /dev/null +++ b/kirby/phpstan.neon.dist @@ -0,0 +1,21 @@ +parameters: + paths: + - %currentWorkingDirectory% + autoload_files: + - %currentWorkingDirectory%/vendor/autoload.php + - %rootDir%/../../autoload.php + autoload_directories: + - %currentWorkingDirectory%/tests + excludes_analyse: + - %currentWorkingDirectory%/dependencies + - %currentWorkingDirectory%/tests/*/fixtures/* + - %currentWorkingDirectory%/vendor + - %currentWorkingDirectory%/views + + level: 0 + memory_limit: 2G + + ignoreErrors: + # we use bound $this in our callbacks + - message: '#(Using \$this outside a class\.|Undefined variable: \$this)#' + path: %currentWorkingDirectory%/config diff --git a/kirby/router.php b/kirby/router.php new file mode 100755 index 0000000..ba27ee0 --- /dev/null +++ b/kirby/router.php @@ -0,0 +1,12 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Api +{ + 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 Route + */ + protected $route; + + /** + * The Router instance + * + * @var 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 + */ + 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 $path + * @param string $method + * @param array $requestData + * @return mixed + */ + 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 + */ + 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; + } + + /** + * Returns an API model instance by name + * + * @param string $name + * @param mixed $object + * @return \Kirby\Api\Model + * + * @throws \Kirby\Exception\NotFoundException If no model for `$name` exists + */ + public function model(string $name, $object = null) + { + 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 $type + * @param string $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 $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 $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 $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 $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; + } + + $className = strtolower(get_class($object)); + $lastDash = strrpos($className, '\\'); + + if ($lastDash !== false) { + $className = substr($className, $lastDash + 1); + } + + if (isset($this->models[$className]) === true) { + return $this->model($className, $object); + } + + if (isset($this->collections[$className]) === true) { + return $this->collection($className, $object); + } + + // now models deeply by checking for the actual type + foreach ($this->models as $modelClass => $model) { + if (is_a($object, $model['type']) === true) { + return $this->model($modelClass, $object); + } + } + + throw new NotFoundException(sprintf('The object "%s" cannot be resolved', $className)); + } + + /** + * Returns all defined routes + * + * @return array + */ + public function routes(): array + { + return $this->routes; + } + + /** + * Setter for the authentication callback + * + * @param Closure $authentication + * @return self + */ + protected function setAuthentication(Closure $authentication = null) + { + $this->authentication = $authentication; + return $this; + } + + /** + * Setter for the collections definition + * + * @param array $collections + * @return self + */ + 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 $data + * @return self + */ + protected function setData(array $data = null) + { + $this->data = $data ?? []; + return $this; + } + + /** + * Setter for the debug flag + * + * @param bool $debug + * @return self + */ + protected function setDebug(bool $debug = false) + { + $this->debug = $debug; + return $this; + } + + /** + * Setter for the model definitions + * + * @param array $models + * @return self + */ + 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 $requestData + * @return self + */ + 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 $requestMethod + * @return self + */ + protected function setRequestMethod(string $requestMethod = null) + { + $this->requestMethod = $requestMethod ?? 'GET'; + return $this; + } + + /** + * Setter for the route definitions + * + * @param array $routes + * @return self + */ + 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 Exception $e + * @return array + */ + public function responseForException($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 + * + * @param Closure $callback + * @param bool $single + * @return array + * + * @throws \Exception If request has no files + * @throws \Exception If there was an error with the upload + */ + public function upload(Closure $callback, $single = 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 (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 + ]; + } +} diff --git a/kirby/src/Api/Collection.php b/kirby/src/Api/Collection.php new file mode 100755 index 0000000..fe40b8a --- /dev/null +++ b/kirby/src/Api/Collection.php @@ -0,0 +1,129 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Collection +{ + protected $api; + protected $data; + protected $model; + protected $select; + protected $view; + + public function __construct(Api $api, $data = null, array $schema) + { + $this->api = $api; + $this->data = $data; + $this->model = $schema['model']; + $this->view = $schema['view'] ?? null; + + if ($data === null) { + if (is_a($schema['default'] ?? null, 'Closure') === false) { + throw new Exception('Missing collection data'); + } + + $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'); + } + } + + public function select($keys = null) + { + if ($keys === false) { + return $this; + } + + if (is_string($keys)) { + $keys = Str::split($keys); + } + + if ($keys !== null && is_array($keys) === false) { + throw new Exception('Invalid select keys'); + } + + $this->select = $keys; + return $this; + } + + public function toArray(): array + { + $result = []; + + foreach ($this->data as $item) { + $model = $this->api->model($this->model, $item); + + if ($this->view !== null) { + $model = $model->view($this->view); + } + + if ($this->select !== null) { + $model = $model->select($this->select); + } + + $result[] = $model->toArray(); + } + + return $result; + } + + 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) + ]); + } + + $pagination = $this->data->pagination(); + + if ($select = $this->api->requestQuery('select')) { + $this->select($select); + } + + 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' + ]; + } + + public function view(string $view) + { + $this->view = $view; + return $this; + } +} diff --git a/kirby/src/Api/Model.php b/kirby/src/Api/Model.php new file mode 100755 index 0000000..1eff4e5 --- /dev/null +++ b/kirby/src/Api/Model.php @@ -0,0 +1,192 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Model +{ + protected $api; + protected $data; + protected $fields; + protected $select; + protected $views; + + public function __construct(Api $api, $data = null, 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 ($data === null) { + if (is_a($schema['default'] ?? null, 'Closure') === false) { + throw new Exception('Missing model data'); + } + + $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'])); + } + } + + public function select($keys = null) + { + if ($keys === false) { + return $this; + } + + if (is_string($keys)) { + $keys = Str::split($keys); + } + + if ($keys !== null && is_array($keys) === false) { + throw new Exception('Invalid select keys'); + } + + $this->select = $keys; + return $this; + } + + public function selection(): array + { + $select = $this->select; + + if ($select === null) { + $select = array_keys($this->fields); + } + + $selection = []; + + 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"'); + } + + $selection[$key] = [ + 'view' => $value, + 'select' => null + ]; + + continue; + } + + if (is_array($value) === true) { + $selection[$key] = [ + 'view' => null, + 'select' => $value + ]; + } + } + + return $selection; + } + + 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; + } + + $value = $resolver->call($this->api, $this->data); + + 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 ($subview = $selection['view']) { + $value->view($subview); + } + + if ($subselect = $selection['select']) { + $value->select($subselect); + } + + $value = $value->toArray(); + } + + $result[$key] = $value; + } + + ksort($result); + + return $result; + } + + public function toResponse(): array + { + $model = $this; + + if ($select = $this->api->requestQuery('select')) { + $model = $model->select($select); + } + + if ($view = $this->api->requestQuery('view')) { + $model = $model->view($view); + } + + return [ + 'code' => 200, + 'data' => $model->toArray(), + 'status' => 'ok', + 'type' => 'model' + ]; + } + + public function view(string $name) + { + if ($name === 'any') { + return $this->select(null); + } + + 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)); + } + } + + return $this->select($this->views[$name]); + } +} diff --git a/kirby/src/Cache/ApcuCache.php b/kirby/src/Cache/ApcuCache.php new file mode 100755 index 0000000..e750d20 --- /dev/null +++ b/kirby/src/Cache/ApcuCache.php @@ -0,0 +1,86 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +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)); + } + + /** + * 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)); + } + + /** + * 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)); + } +} diff --git a/kirby/src/Cache/Cache.php b/kirby/src/Cache/Cache.php new file mode 100755 index 0000000..729d61b --- /dev/null +++ b/kirby/src/Cache/Cache.php @@ -0,0 +1,242 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +abstract class Cache +{ + /** + * 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; + } + + /** + * 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; + } + + 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); + + /** + * 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; + } + + // 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(); + } + + /** + * 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); + } + + /** + * 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; + } + + // 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); + + 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); + + // check for a valid Value object + if (!is_a($value, 'Kirby\Cache\Value')) { + return false; + } + + // 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); + } + + /** + * 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; + + /** + * 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; + } +} diff --git a/kirby/src/Cache/FileCache.php b/kirby/src/Cache/FileCache.php new file mode 100755 index 0000000..1e50ff4 --- /dev/null +++ b/kirby/src/Cache/FileCache.php @@ -0,0 +1,155 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class FileCache extends Cache +{ + /** + * 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 + ]; + + 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']; + } + + // try to create the directory + Dir::make($this->root, true); + } + + /** + * Returns the full path to a file for a given key + * + * @param string $key + * @return string + */ + protected function file(string $key): string + { + $file = $this->root . '/' . $key; + + 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); + + 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); + + return Value::fromJson(F::read($file)); + } + + /** + * 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; + } + + /** + * 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) { + return F::remove($file); + } else { + return false; + } + } + + /** + * 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 + } +} diff --git a/kirby/src/Cache/MemCached.php b/kirby/src/Cache/MemCached.php new file mode 100755 index 0000000..82cff09 --- /dev/null +++ b/kirby/src/Cache/MemCached.php @@ -0,0 +1,97 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class MemCached extends Cache +{ + /** + * store for the memache 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, + ]; + + parent::__construct(array_merge($defaults, $options)); + + $this->connection = new \Memcached(); + $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)); + } + + /** + * 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)); + } + + /** + * 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 new file mode 100755 index 0000000..7f2d098 --- /dev/null +++ b/kirby/src/Cache/MemoryCache.php @@ -0,0 +1,82 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class MemoryCache extends Cache +{ + /** + * 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; + } + + /** + * 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; + } + } + + /** + * 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 new file mode 100755 index 0000000..a33fc9c --- /dev/null +++ b/kirby/src/Cache/NullCache.php @@ -0,0 +1,69 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +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; + } + + /** + * 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; + } + + /** + * 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 new file mode 100755 index 0000000..038965c --- /dev/null +++ b/kirby/src/Cache/Value.php @@ -0,0 +1,144 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Value +{ + /** + * Cached value + * @var mixed + */ + protected $value; + + /** + * the number of minutes until the value expires + * @var int + */ + protected $minutes; + + /** + * Creation timestamp + * @var int + */ + protected $created; + + /** + * Constructor + * + * @param mixed $value + * @param int $minutes the number of minutes until the value expires + * @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 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; + } + + return $this->created + ($this->minutes * 60); + } + + /** + * Creates a value object from an array + * + * @param array $array + * @return self + */ + 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 self|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; + } + } + + /** + * 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, + ]; + } + + /** + * 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 new file mode 100755 index 0000000..08c9a9e --- /dev/null +++ b/kirby/src/Cms/Api.php @@ -0,0 +1,270 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Api extends BaseApi +{ + /** + * @var App + */ + protected $kirby; + + /** + * Execute an API call for the given path, + * request method and optional request data + * + * @param string $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()); + + if ($user = $this->kirby->user()) { + $this->kirby->setCurrentTranslation($user->language()); + } + + return parent::call($path, $method, $requestData); + } + + /** + * @param mixed $model + * @param string $name + * @param string $path + * @return mixed + */ + public function fieldApi($model, string $name, string $path = null) + { + $form = Form::for($model); + $fieldNames = Str::split($name, '+'); + $index = 0; + $count = count($fieldNames); + $field = null; + + foreach ($fieldNames as $fieldName) { + $index++; + + if ($field = $form->fields()->get($fieldName)) { + if ($count !== $index) { + $form = $field->form(); + } + } else { + throw new NotFoundException('The field "' . $fieldName . '" could not be found'); + } + } + + if ($field === null) { + throw new NotFoundException('The field "' . $fieldNames . '" could not be found'); + } + + $fieldApi = $this->clone([ + 'routes' => $field->api(), + 'data' => array_merge($this->data(), ['field' => $field]) + ]); + + return $fieldApi->call($path, $this->requestMethod(), $this->requestData()); + } + + /** + * Returns the file object for the given + * parent path and filename + * + * @param string $path Path to file's parent model + * @param string $filename Filename + * @return \Kirby\Cms\File|null + */ + public function file(string $path = null, string $filename) + { + $filename = urldecode($filename); + + if ($file = $this->parent($path)->file($filename)) { + return $file; + } + + throw new NotFoundException([ + 'key' => 'file.notFound', + 'data' => [ + 'filename' => $filename + ] + ]); + } + + /** + * Returns the model's object for the given path + * + * @param string $path Path to parent model + * @return \Kirby\Cms\Model|null + */ + public function parent(string $path) + { + $modelType = in_array($path, ['site', 'account']) ? $path : trim(dirname($path), '/'); + $modelTypes = [ + 'site' => 'site', + 'users' => 'user', + 'pages' => 'page', + 'account' => 'account' + ]; + $modelName = $modelTypes[$modelType] ?? null; + + if (Str::endsWith($modelType, '/files') === true) { + $modelName = 'file'; + } + + $kirby = $this->kirby(); + + switch ($modelName) { + case 'site': + $model = $kirby->site(); + break; + case 'account': + $model = $kirby->user(); + break; + case 'page': + $id = str_replace(['+', ' '], '/', basename($path)); + $model = $kirby->page($id); + break; + case 'file': + $model = $this->file(...explode('/files/', $path)); + break; + case 'user': + $model = $kirby->user(basename($path)); + break; + default: + throw new InvalidArgumentException('Invalid file model type: ' . $modelType); + } + + if ($model) { + return $model; + } + + throw new NotFoundException([ + 'key' => $modelName . '.undefined' + ]); + } + + /** + * 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 page object for the given id + * + * @param string $id Page's id + * @return \Kirby\Cms\Page|null + */ + public function page(string $id) + { + $id = str_replace('+', '/', $id); + $page = $this->kirby->page($id); + + if ($page && $page->isReadable()) { + return $page; + } + + throw new NotFoundException([ + 'key' => 'page.notFound', + 'data' => [ + 'slug' => $id + ] + ]); + } + + public function session(array $options = []) + { + return $this->kirby->session(array_merge([ + 'detect' => true + ], $options)); + } + + /** + * @param \Kirby\Cms\App $kirby + */ + 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 user object for the given id or + * returns the current authenticated user if no + * id is passed + * + * @param string $id User's id + * @return \Kirby\Cms\User|null + */ + public function user(string $id = null) + { + // get the authenticated user + if ($id === null) { + return $this->kirby->auth()->user(); + } + + // get a specific user by id + if ($user = $this->kirby->users()->find($id)) { + return $user; + } + + throw new NotFoundException([ + 'key' => 'user.notFound', + 'data' => [ + 'name' => $id + ] + ]); + } + + /** + * 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 new file mode 100755 index 0000000..2db02fc --- /dev/null +++ b/kirby/src/Cms/App.php @@ -0,0 +1,1398 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class App +{ + const CLASS_ALIAS = 'kirby'; + + use AppCaches; + use AppErrors; + use AppPlugins; + use AppTranslations; + use AppUsers; + use Properties; + + protected static $instance; + protected static $root; + protected static $version; + + public $data = []; + + protected $api; + protected $collections; + protected $defaultLanguage; + 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 + */ + public function __construct(array $props = []) + { + // the kirby folder directory + static::$root = dirname(__DIR__, 2); + + // register all roots to be able to load stuff afterwards + $this->bakeRoots($props['roots'] ?? []); + + // stuff from config and additional options + $this->optionsFromConfig(); + $this->optionsFromProps($props['options'] ?? []); + + // 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 + 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'); + + // handle those damn errors + $this->handleErrors(); + + // execute a ready callback from the config + $this->optionsFromReadyCallback(); + + // bake config + Config::$data = $this->options; + } + + /** + * 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 = static::$root . '/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; + * the value that gets modified by the hooks + * is always the last argument + * + * @internal + * @param string $name Hook name + * @param mixed ...$args Arguments to pass to the hooks + * @return mixed Resulting value as modified by the hooks + */ + public function apply(string $name, ...$args) + { + // split up args into "passive" args and the value + $value = array_pop($args); + + if ($functions = $this->extension('hooks', $name)) { + foreach ($functions as $function) { + // re-assemble args + $hookArgs = $args; + $hookArgs[] = $value; + + // bind the App object to the hook + $newValue = $function->call($this, ...$hookArgs); + + // update value if one was returned + if ($newValue !== null) { + $value = $newValue; + } + } + } + + return $value; + } + + /** + * Sets the directory structure + * + * @param array $roots + * @return self + */ + protected function bakeRoots(array $roots = null) + { + $roots = array_merge(require static::$root . '/config/roots.php', (array)$roots); + $this->roots = Ingredients::bake($roots); + return $this; + } + + /** + * Sets the Url structure + * + * @param array $urls + * @return self + */ + protected function bakeUrls(array $urls = null) + { + // inject the index URL from the config + if (isset($this->options['url']) === true) { + $urls['index'] = $this->options['url']; + } + + $urls = array_merge(require static::$root . '/config/urls.php', (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 $path + * @param string $method + * @return mixed + */ + public function call(string $path = null, string $method = null) + { + $router = $this->router(); + + $router::$beforeEach = function ($route, $path, $method) { + $this->trigger('route:before', $route, $path, $method); + }; + + $router::$afterEach = function ($route, $path, $method, $result) { + return $this->apply('route:after', $route, $path, $method, $result); + }; + + return $router->call($path ?? $this->path(), $method ?? $this->request()->method()); + } + + /** + * 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; + } + + /** + * 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; + } + + /** + * 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 prefered 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\PHPMailer + */ + public function email($preset = [], array $props = []) + { + return new Emailer((new Email($preset, $props))->toArray(), $props['debug'] ?? false); + } + + /** + * 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 $instance + * @return self + */ + public static function instance(self $instance = null) + { + if ($instance === null) { + 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(); + $message = $input->getMessage(); + } 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 $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 $text + * @param array $data + * @return string + */ + public function kirbytags(string $text = null, array $data = []): string + { + $data['kirby'] = $data['kirby'] ?? $this; + $data['site'] = $data['site'] ?? $data['kirby']->site(); + $data['parent'] = $data['parent'] ?? $data['site']->page(); + + return KirbyTags::parse($text, $data, $this->options, $this->extensions['hooks']); + } + + /** + * Parses KirbyTags first and Markdown afterwards + * + * @internal + * @param string $text + * @param array $data + * @param bool $inline + * @return string + */ + public function kirbytext(string $text = null, array $data = [], bool $inline = false): string + { + $text = $this->apply('kirbytext:before', $text); + $text = $this->kirbytags($text, $data); + $text = $this->markdown($text, $inline); + + if ($this->option('smartypants', false) !== false) { + $text = $this->smartypants($text); + } + + $text = $this->apply('kirbytext:after', $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 + * + * @return \Kirby\Cms\Languages + */ + public function languages() + { + if ($this->languages !== null) { + return clone $this->languages; + } + + return $this->languages = Languages::load(); + } + + /** + * 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 $text + * @param bool $inline + * @return string + */ + public function markdown(string $text = null, bool $inline = false): string + { + return $this->component('markdown')($this, $text, $this->options['markdown'] ?? [], $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 + { + $server = $this->server(); + $root = $this->root('config'); + + Config::$data = []; + + $main = F::load($root . '/config.php', []); + $host = F::load($root . '/config.' . basename($server->host()) . '.php', []); + $addr = F::load($root . '/config.' . basename($server->address()) . '.php', []); + + $config = Config::$data; + + return $this->options = array_replace_recursive($config, $main, $host, $addr); + } + + /** + * 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); + } + + return $this->options; + } + + /** + * Returns any page from the content folder + * + * @param string $id|null + * @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)) { + 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 $path + * @param string|null $language + * @return mixed + */ + 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 { + return $this + ->response() + ->body($page->render([], $extension)) + ->type($extension); + } 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 = (include static::$root . '/config/routes.php')($this); + $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 = []) + { + 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 $languages + * @return self + */ + 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 $path + * @return self + */ + protected function setPath(string $path = null) + { + $this->path = $path !== null ? trim($path, '/') : null; + return $this; + } + + /** + * Sets the request + * + * @param array $request + * @return self + */ + protected function setRequest(array $request = null) + { + if ($request !== null) { + $this->request = new Request($request); + } + + return $this; + } + + /** + * Create your own set of roles + * + * @param array $roles + * @return self + */ + 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 $site + * @return self + */ + 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 = $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 $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 + * @param mixed ...$arguments + * @return void + */ + public function trigger(string $name, ...$arguments) + { + 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 + $function->call($this, ...$arguments); + } + + $level--; + + if ($level === 0) { + $triggered = []; + } + } + } + + /** + * Returns a system url + * + * @param string $type + * @return string + */ + public function url(string $type = 'index'): string + { + return $this->urls->__get($type); + } + + /** + * 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 + */ + public static function version(): ?string + { + return static::$version = static::$version ?? Data::read(static::$root . '/composer.json')['version'] ?? null; + } + + /** + * Creates a hash of the version number + * + * @return string + */ + public static function versionHash(): string + { + return md5(static::version()); + } + + /** + * Returns the visitor object + * + * @return \Kirby\Cms\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 new file mode 100755 index 0000000..862bc38 --- /dev/null +++ b/kirby/src/Cms/AppCaches.php @@ -0,0 +1,138 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait AppCaches +{ + 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]; + } + + // 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(); + } + + $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] + ]); + } + + $className = $types[$type]; + + // initialize the cache class + $cache = new $className($options); + + // check if it is a useable 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; + } + + /** + * Returns the cache options by key + * + * @param string $key + * @return array + */ + protected function cacheOptions(string $key): array + { + $options = $this->option($cacheKey = $this->cacheOptionsKey($key), false); + + if ($options === false) { + return [ + 'active' => false + ]; + } + + $prefix = str_replace(['/', ':'], '_', $this->system()->indexUrl()) . + '/' . + str_replace('.', '/', $key); + + $defaults = [ + 'active' => true, + 'type' => 'file', + 'extension' => 'cache', + 'root' => $this->root('cache'), + 'prefix' => $prefix + ]; + + 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; + + 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; + } + + // 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; + } + + return $prefixedKey; + } +} diff --git a/kirby/src/Cms/AppErrors.php b/kirby/src/Cms/AppErrors.php new file mode 100755 index 0000000..6936d0d --- /dev/null +++ b/kirby/src/Cms/AppErrors.php @@ -0,0 +1,118 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait AppErrors +{ + protected function handleCliErrors(): void + { + $whoops = new Whoops(); + $whoops->pushHandler(new PlainTextHandler()); + $whoops->register(); + } + + protected function handleErrors() + { + if ($this->request()->cli() === true) { + $this->handleCliErrors(); + return; + } + + if ($this->visitor()->prefersJson() === true) { + $this->handleJsonErrors(); + return; + } + + $this->handleHtmlErrors(); + } + + protected function handleHtmlErrors() + { + $whoops = new Whoops(); + + if ($this->option('debug') === true) { + if ($this->option('whoops', true) === true) { + $handler = new PrettyPageHandler(); + $handler->setPageTitle('Kirby CMS Debugger'); + + if ($editor = $this->option('editor')) { + $handler->setEditor($editor); + } + + $whoops->pushHandler($handler); + $whoops->register(); + } + } else { + $handler = new CallbackHandler(function ($exception, $inspector, $run) { + $fatal = $this->option('fatal'); + + if (is_a($fatal, 'Closure') === true) { + echo $fatal($this); + } else { + include static::$root . '/views/fatal.php'; + } + + return Handler::QUIT; + }); + + $whoops->pushHandler($handler); + $whoops->register(); + } + } + + protected function handleJsonErrors() + { + $whoops = new Whoops(); + $handler = new CallbackHandler(function ($exception, $inspector, $run) { + if (is_a($exception, 'Kirby\Exception\Exception') === true) { + $httpCode = $exception->getHttpCode(); + $code = $exception->getCode(); + $details = $exception->getDetails(); + } else { + $httpCode = 500; + $code = $exception->getCode(); + $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'] ?? null), + 'line' => $exception->getLine(), + ], $httpCode); + } else { + echo Response::json([ + 'status' => 'error', + 'code' => $code, + 'details' => $details, + 'message' => 'An unexpected error occurred! Enable debug mode for more info: https://getkirby.com/docs/reference/system/options/debug', + ], $httpCode); + } + + return Handler::QUIT; + }); + + $whoops->pushHandler($handler); + $whoops->register(); + } +} diff --git a/kirby/src/Cms/AppPlugins.php b/kirby/src/Cms/AppPlugins.php new file mode 100755 index 0000000..121e4b1 --- /dev/null +++ b/kirby/src/Cms/AppPlugins.php @@ -0,0 +1,743 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +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' => [], + 'blueprints' => [], + 'cacheTypes' => [], + 'collections' => [], + 'components' => [], + 'controllers' => [], + 'collectionFilters' => [], + 'fieldMethods' => [], + 'fileMethods' => [], + 'filesMethods' => [], + 'fields' => [], + 'hooks' => [], + 'pages' => [], + 'pageMethods' => [], + 'pagesMethods' => [], + 'pageModels' => [], + 'routes' => [], + 'sections' => [], + 'siteMethods' => [], + 'snippets' => [], + 'tags' => [], + 'templates' => [], + 'translations' => [], + 'userMethods' => [], + 'userModels' => [], + 'usersMethods' => [], + 'validators' => [] + ]; + + /** + * Cache for system extensions + * + * @var array + */ + protected static $systemExtensions = null; + + /** + * 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 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 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'] = Collection::$filters = array_merge(Collection::$filters, $filters); + } + + /** + * 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 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 options + * + * @param array $options + * @param \Kirby\Cms\Plugin|null $plugin + * @return array + */ + protected function extendOptions(array $options, Plugin $plugin = null): array + { + if ($plugin !== null) { + $prefixed = []; + + foreach ($options as $key => $value) { + $prefixed[$plugin->prefix() . '.' . $key] = $value; + } + + $options = $prefixed; + } + + 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 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); + } + + /** + * 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 + include_once $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 array + */ + 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 + * + * @param array $plugins + * @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() + { + // load static extensions only once + if (static::$systemExtensions === null) { + // Form Field Mixins + FormField::$mixins['filepicker'] = include static::$root . '/config/fields/mixins/filepicker.php'; + FormField::$mixins['min'] = include static::$root . '/config/fields/mixins/min.php'; + FormField::$mixins['options'] = include static::$root . '/config/fields/mixins/options.php'; + FormField::$mixins['pagepicker'] = include static::$root . '/config/fields/mixins/pagepicker.php'; + FormField::$mixins['picker'] = include static::$root . '/config/fields/mixins/picker.php'; + FormField::$mixins['upload'] = include static::$root . '/config/fields/mixins/upload.php'; + FormField::$mixins['userpicker'] = include static::$root . '/config/fields/mixins/userpicker.php'; + + // Tag Aliases + KirbyTag::$aliases = [ + 'youtube' => 'video', + 'vimeo' => 'video' + ]; + + // Field method aliases + Field::$aliases = [ + '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' + ]; + + // blueprint presets + PageBlueprint::$presets['pages'] = include static::$root . '/config/presets/pages.php'; + PageBlueprint::$presets['page'] = include static::$root . '/config/presets/page.php'; + PageBlueprint::$presets['files'] = include static::$root . '/config/presets/files.php'; + + // section mixins + Section::$mixins['empty'] = include static::$root . '/config/sections/mixins/empty.php'; + Section::$mixins['headline'] = include static::$root . '/config/sections/mixins/headline.php'; + Section::$mixins['help'] = include static::$root . '/config/sections/mixins/help.php'; + Section::$mixins['layout'] = include static::$root . '/config/sections/mixins/layout.php'; + Section::$mixins['max'] = include static::$root . '/config/sections/mixins/max.php'; + Section::$mixins['min'] = include static::$root . '/config/sections/mixins/min.php'; + Section::$mixins['pagination'] = include static::$root . '/config/sections/mixins/pagination.php'; + Section::$mixins['parent'] = include static::$root . '/config/sections/mixins/parent.php'; + + // section types + Section::$types['info'] = include static::$root . '/config/sections/info.php'; + Section::$types['pages'] = include static::$root . '/config/sections/pages.php'; + Section::$types['files'] = include static::$root . '/config/sections/files.php'; + Section::$types['fields'] = include static::$root . '/config/sections/fields.php'; + + static::$systemExtensions = [ + 'components' => include static::$root . '/config/components.php', + 'blueprints' => include static::$root . '/config/blueprints.php', + 'fields' => include static::$root . '/config/fields.php', + 'fieldMethods' => include static::$root . '/config/methods.php', + 'tags' => include static::$root . '/config/tags.php' + ]; + } + + // default cache types + $this->extendCacheTypes([ + 'apcu' => 'Kirby\Cache\ApcuCache', + 'file' => 'Kirby\Cache\FileCache', + 'memcached' => 'Kirby\Cache\MemCached', + 'memory' => 'Kirby\Cache\MemoryCache', + ]); + + $this->extendComponents(static::$systemExtensions['components']); + $this->extendBlueprints(static::$systemExtensions['blueprints']); + $this->extendFields(static::$systemExtensions['fields']); + $this->extendFieldMethods((static::$systemExtensions['fieldMethods'])($this)); + $this->extendTags(static::$systemExtensions['tags']); + } + + /** + * 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 + */ + 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 $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; + } + + include_once $entry; + + $loaded[] = $dir; + } + + return $loaded; + } +} diff --git a/kirby/src/Cms/AppTranslations.php b/kirby/src/Cms/AppTranslations.php new file mode 100755 index 0000000..073addf --- /dev/null +++ b/kirby/src/Cms/AppTranslations.php @@ -0,0 +1,177 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait AppTranslations +{ + protected $translations; + + /** + * Setup internationalization + * + * @return void + */ + protected function i18n(): void + { + I18n::$load = function ($locale): array { + $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()); + + // Add language slug rules to Str class + Str::$language = $language->rules(); + } + + + return $data; + }; + + I18n::$locale = function (): string { + if ($this->multilang() === true) { + return $this->defaultLanguage()->code(); + } else { + return 'en'; + } + }; + + I18n::$fallback = function (): string { + if ($this->multilang() === true) { + return $this->defaultLanguage()->code(); + } else { + return 'en'; + } + }; + + I18n::$translations = []; + + if (isset($this->options['slugs']) === true) { + $file = $this->root('i18n:rules') . '/' . $this->options['slugs'] . '.json'; + + if (F::exists($file) === true) { + try { + $data = Data::read($file); + } catch (\Exception $e) { + $data = []; + } + + Str::$language = $data; + } + } + } + + /** + * Load and set the current language if it exists + * Otherwise fall back to the default language + * + * @internal + * @param string $languageCode + * @return \Kirby\Cms\Language|null + */ + public function setCurrentLanguage(string $languageCode = null) + { + if ($this->multilang() === false) { + $this->setLocale($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 ($this->language) { + $this->setLocale($this->language->locale()); + } + + return $this->language; + } + + /** + * Set the current translation + * + * @internal + * @param string $translationCode + * @return void + */ + public function setCurrentTranslation(string $translationCode = null): void + { + I18n::$locale = $translationCode ?? 'en'; + } + + /** + * Set locale settings + * + * @internal + * @param string|array $locale + */ + public function setLocale($locale): void + { + if (is_array($locale) === true) { + foreach ($locale as $key => $value) { + setlocale($key, $value); + } + } else { + setlocale(LC_ALL, $locale); + } + } + + /** + * Load a specific translation by locale + * + * @param string|null $locale + * @return \Kirby\Cms\Translation|null + */ + 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; + } + } + + // get injected translation data from plugins etc. + $inject = $this->extensions['translations'][$locale] ?? []; + + // 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; + } + + return Translations::load($this->root('i18n:translations'), $this->extensions['translations'] ?? []); + } +} diff --git a/kirby/src/Cms/AppUsers.php b/kirby/src/Cms/AppUsers.php new file mode 100755 index 0000000..98697f2 --- /dev/null +++ b/kirby/src/Cms/AppUsers.php @@ -0,0 +1,113 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait AppUsers +{ + /** + * 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); + } + + /** + * Become any existing user + * + * @param string|null $who + * @return \Kirby\Cms\User|null + */ + public function impersonate(string $who = null) + { + return $this->auth()->impersonate($who); + } + + /** + * 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 $users + * @return \Kirby\Cms\App + */ + protected function setUsers(array $users = null) + { + if ($users !== null) { + $this->users = Users::factory($users, [ + 'kirby' => $this + ]); + } + + return $this; + } + + /** + * Returns a specific user by id + * or the current user if no id is given + * + * @param string $id + * @return \Kirby\Cms\User|null + */ + public function user(string $id = null) + { + if ($id !== null) { + return $this->users()->find($id); + } + + if (is_string($this->user) === true) { + return $this->auth()->impersonate($this->user); + } else { + try { + return $this->auth()->user(); + } 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; + } + + return $this->users = Users::load($this->root('accounts'), ['kirby' => $this]); + } +} diff --git a/kirby/src/Cms/Asset.php b/kirby/src/Cms/Asset.php new file mode 100755 index 0000000..a722238 --- /dev/null +++ b/kirby/src/Cms/Asset.php @@ -0,0 +1,126 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Asset +{ + use FileFoundation; + use FileModifications; + use Properties; + + /** + * @var string + */ + protected $path; + + /** + * Creates a new Asset object + * for the given path. + * + * @param string $path + */ + public function __construct(string $path) + { + $this->setPath(dirname($path)); + $this->setRoot($this->kirby()->root('index') . '/' . $path); + $this->setUrl($this->kirby()->url('index') . '/' . $path); + } + + /** + * Returns the alternative text for the asset + * + * @return null + */ + public function alt() + { + return null; + } + + /** + * 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(); + } + + /** + * 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 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; + } + + /** + * Setter for the path + * + * @param string $path + * @return self + */ + protected function setPath(string $path) + { + $this->path = $path === '.' ? '' : $path; + return $this; + } +} diff --git a/kirby/src/Cms/Auth.php b/kirby/src/Cms/Auth.php new file mode 100755 index 0000000..bd2b6dd --- /dev/null +++ b/kirby/src/Cms/Auth.php @@ -0,0 +1,476 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Auth +{ + protected $impersonate; + protected $kirby; + protected $user = false; + protected $userException; + + /** + * @param \Kirby\Cms\App $kirby + * @codeCoverageIgnore + */ + public function __construct(App $kirby) + { + $this->kirby = $kirby; + } + + /** + * 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->kirby->option('api.csrf', csrf()); + + // compare both tokens + if (hash_equals((string)$fromSession, (string)$fromHeader) !== true) { + return false; + } + + return $fromSession; + } + + /** + * 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 + */ + public function currentUserFromBasicAuth(BasicAuth $auth = null) + { + if ($this->kirby->option('api.basicAuth', false) !== true) { + throw new PermissionException('Basic authentication is not activated'); + } + + $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 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) + { + // use passed session options or session object if set + if (is_array($session) === true) { + $session = $this->kirby->session($session); + } + + // try session in header or cookie + if (is_a($session, 'Kirby\Session\Session') === false) { + $session = $this->kirby->session(['detect' => true]); + } + + $id = $session->data()->get('user.id'); + + 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; + } + + /** + * Become any existing user + * + * @param string|null $who + * @return \Kirby\Cms\User|null + */ + public function impersonate(string $who = null) + { + switch ($who) { + case null: + return $this->impersonate = null; + case 'kirby': + return $this->impersonate = new User([ + 'email' => 'kirby@getkirby.com', + 'id' => 'kirby', + 'role' => 'admin', + ]); + 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 occured 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); + + return $user; + } + + /** + * 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; + } + + /** + * 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 occured 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) + { + // check for blocked ips + if ($this->isBlocked($email) === true) { + if ($this->kirby->option('debug') === true) { + $message = 'Rate limit exceeded'; + } else { + // avoid leaking security-relevant information + $message = 'Invalid email or password'; + } + + throw new PermissionException($message); + } + + // 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('Invalid email or password'); + } + } + } + + /** + * 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, function ($entry) use ($time) { + return $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(); + } + } + + /** + * Clears the cached user data after logout + * @internal + * + * @return void + */ + public function flush(): void + { + $this->impersonate = null; + $this->user = null; + } + + /** + * Tracks a login + * + * @param string $email + * @return bool + */ + public function track(string $email): bool + { + $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 ($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 + * + * @return string + */ + public function type(): string + { + $basicAuth = $this->kirby->option('api.basicAuth', false); + $auth = $this->kirby->request()->auth(); + + if ($basicAuth === true && $auth && $auth->type() === 'basic') { + return 'basic'; + } elseif ($this->impersonate !== null) { + return 'impersonate'; + } else { + return 'session'; + } + } + + /** + * Validates the currently logged in user + * + * @param \Kirby\Session\Session|array|null $session + * @return \Kirby\Cms\User + * + * @throws \Throwable If an authentication error occured + */ + public function user($session = null) + { + if ($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; + } + } +} diff --git a/kirby/src/Cms/Blueprint.php b/kirby/src/Cms/Blueprint.php new file mode 100755 index 0000000..fb7003d --- /dev/null +++ b/kirby/src/Cms/Blueprint.php @@ -0,0 +1,797 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +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 $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 + */ + public function __construct(array $props) + { + if (empty($props['model']) === true) { + throw new InvalidArgumentException('A blueprint model is required'); + } + + $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'] = $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; + } + + $mixin = static::find($extends); + + if ($mixin === null) { + $props = $props; + } elseif (is_array($mixin) === true) { + $props = A::merge($mixin, $props, A::MERGE_REPLACE); + } else { + try { + $props = A::merge(Data::read($mixin), $props, A::MERGE_REPLACE); + } catch (Exception $e) { + $props = $props; + } + } + + // remove the extends flag + unset($props['extends']); + return $props; + } + + /** + * Create a new blueprint for a model + * + * @param string $name + * @param string $fallback + * @param \Kirby\Cms\Model $model + * @return self + */ + 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 string|array + */ + public static function find(string $name) + { + $kirby = App::instance(); + $root = $kirby->root('blueprints'); + $file = $root . '/' . $name . '.yml'; + + if (F::exists($file, $root) === true) { + return $file; + } + + if ($blueprint = $kirby->extension('blueprints', $name)) { + return $blueprint; + } + + 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 + { + if (isset(static::$loaded[$name]) === true) { + return static::$loaded[$name]; + } + + $props = static::find($name); + $normalize = function ($props) use ($name) { + + // inject the filename as name if no name is set + $props['name'] = $props['name'] ?? $name; + + // normalize the title + $title = $props['title'] ?? ucfirst($props['name']); + + // translate the title + $props['title'] = I18n::translate($title, $title); + + return $props; + }; + + if (is_array($props) === true) { + return $normalize($props); + } + + $file = $props; + $props = Data::read($file); + + return static::$loaded[$name] = $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) { + if (is_array($columnProps) === false) { + 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; + } + + 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 + */ + 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 propperty 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(function () { + return false; + }, $defaults); + } + + // extend options if possible + $options = $this->extend($options); + + foreach ($options as $key => $value) { + $alias = $aliases[$key] ?? null; + + if ($alias !== null) { + $options[$alias] = $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 propperty 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 propperty 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)), + '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; + } + + return static::$presets[$props['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 array_map(function ($section) { + return $this->section($section['name']); + }, $this->sections); + } + + /** + * Returns a single tab by name + * + * @param string $name + * @return array|null + */ + public function tab(string $name): ?array + { + 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 new file mode 100755 index 0000000..7f65b95 --- /dev/null +++ b/kirby/src/Cms/Collection.php @@ -0,0 +1,333 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Collection extends BaseCollection +{ + use HasMethods; + + /** + * 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)) { + return $this->callMethod($key, $arguments); + } + } + + /** + * Creates a new Collection with the given objects + * + * @param array $objects + * @param object $parent + */ + public function __construct($objects = [], $parent = null) + { + $this->parent = $parent; + + 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; + } + + /** + * Adds a single object or + * an entire second collection to the + * current collection + * + * @param mixed $object + */ + public function add($object) + { + if (is_a($object, static::class) === true) { + $this->data = array_merge($this->data, $object->data); + } elseif (method_exists($object, 'id') === true) { + $this->__set($object->id(), $object); + } else { + $this->append($object); + } + + return $this; + } + + /** + * Appends an element to the data array + * + * @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); + } + + /** + * Groups the items by a given field. Returns a collection + * with an item for each group and a collection for each group. + * + * @param string $field + * @param bool $i Ignore upper/lowercase for group names + * @return \Kirby\Cms\Collection + */ + public function groupBy($field, bool $i = true) + { + if (is_string($field) === false) { + throw new Exception('Cannot group by non-string values. Did you mean to call group()?'); + } + + $groups = new Collection([], $this->parent()); + + 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); + } + + // 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); + } + } + + return $groups; + } + + /** + * Checks if the given object or id + * is in the collection + * + * @param string|object $id + * @return bool + */ + public function has($id): bool + { + if (is_object($id) === true) { + $id = $id->id(); + } + + return parent::has($id); + } + + /** + * Correct position detection for objects. + * The method will automatically detect objects + * or ids and then search accordingly. + * + * @param string|object $object + * @return int + */ + public function indexOf($object): int + { + if (is_string($object) === true) { + return array_search($object, $this->keys()); + } + + return array_search($object->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(); + foreach ($keys as $key) { + if (is_a($key, 'Kirby\Toolkit\Collection') === true) { + $collection = $collection->not(...$key->keys()); + } elseif (is_object($key) === true) { + $key = $key->id(); + } + unset($collection->$key); + } + 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); + + // 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; + } + + /** + * Prepends an element to the data array + * + * @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); + } + + /** + * Runs a combination of filterBy, sortBy, not + * offset, limit, search and paginate on the collection. + * Any part of the query is optional. + * + * @param array $query + * @return self + */ + public function query(array $query = []) + { + $paginate = $query['paginate'] ?? null; + $search = $query['search'] ?? null; + + unset($query['paginate']); + + $result = parent::query($query); + + 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); + } + + 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(); + } + + return parent::remove($key); + } + + /** + * Searches the collection + * + * @param string $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 $map + * @return array + */ + public function toArray(Closure $map = null): array + { + return parent::toArray($map ?? function ($object) { + return $object->toArray(); + }); + } +} diff --git a/kirby/src/Cms/Collections.php b/kirby/src/Cms/Collections.php new file mode 100755 index 0000000..8a42ce2 --- /dev/null +++ b/kirby/src/Cms/Collections.php @@ -0,0 +1,139 @@ +collection()` + * method to provide easy access to registered collections + * + * @package Kirby Cms + * @author Bastian Allgeier + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +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 = []; + + /** + * 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); + } + + /** + * 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 + ]; + } + + // return cloned object + if (is_object($this->cache[$name]['result']) === true) { + return clone $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; + } + + 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 + */ + public function load(string $name) + { + $kirby = App::instance(); + + // first check for collection file + $file = $kirby->root('collections') . '/' . $name . '.php'; + + if (file_exists($file)) { + $collection = require $file; + + if (is_a($collection, 'Closure')) { + return $collection; + } + } + + // fallback to collections from plugins + $collections = $kirby->extensions('collections'); + + if (isset($collections[$name]) === true) { + return $collections[$name]; + } + + throw new NotFoundException('The collection cannot be found'); + } +} diff --git a/kirby/src/Cms/Content.php b/kirby/src/Cms/Content.php new file mode 100755 index 0000000..e55d84a --- /dev/null +++ b/kirby/src/Cms/Content.php @@ -0,0 +1,262 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Content +{ + /** + * 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 = []; + + /** + * 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); + } + + /** + * 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; + } + + /** + * Same as `self::data()` to improve + * `var_dump` output + * + * @see self::data() + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } + + /** + * Converts the content to a new blueprint + * + * @param string $to + * @return array + */ + public function convertTo(string $to): array + { + // prepare data + $data = []; + $content = $this; + + // blueprints + $old = $this->parent->blueprint(); + $subfolder = dirname($old->name()); + $new = Blueprint::factory($subfolder . '/' . $to, $subfolder . '/default', $this->parent); + + // forms + $oldForm = new Form(['fields' => $old->fields(), 'model' => $this->parent]); + $newForm = new Form(['fields' => $new->fields(), 'model' => $this->parent]); + + // fields + $oldFields = $oldForm->fields(); + $newFields = $newForm->fields(); + + // go through all fields of new template + foreach ($newFields as $newField) { + $name = $newField->name(); + $oldField = $oldFields->get($name); + + // 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(); + } + } + + // preserve existing fields + return array_merge($this->data, $data); + } + + /** + * Returns the raw data array + * + * @return array + */ + public function data(): array + { + return $this->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 either a single field object + * or all registered fields + * + * @param string $key + * @return \Kirby\Cms\Field|array + */ + public function get(string $key = null) + { + if ($key === null) { + return $this->fields(); + } + + $key = strtolower($key); + + if (isset($this->fields[$key])) { + return $this->fields[$key]; + } + + // fetch the value no matter the case + $data = $this->data(); + $value = $data[$key] ?? array_change_key_case($data)[$key] ?? null; + + return $this->fields[$key] = new Field($this->parent, $key, $value); + } + + /** + * 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 isset($data[$key]) === true; + } + + /** + * 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 self + */ + public function not(...$keys) + { + $copy = clone $this; + $copy->fields = null; + + foreach ($keys as $key) { + unset($copy->data[$key]); + } + + return $copy; + } + + /** + * 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 self + */ + 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(); + } + + /** + * Updates the content and returns + * a cloned object + * + * @param array $content + * @param bool $overwrite + * @return self + */ + public function update(array $content = null, bool $overwrite = false) + { + $this->data = $overwrite === true ? (array)$content : array_merge($this->data, (array)$content); + return $this; + } +} diff --git a/kirby/src/Cms/ContentLock.php b/kirby/src/Cms/ContentLock.php new file mode 100755 index 0000000..44ec6c4 --- /dev/null +++ b/kirby/src/Cms/ContentLock.php @@ -0,0 +1,216 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class ContentLock +{ + /** + * Lock data + * + * @var array + */ + protected $data; + + /** + * 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); + } + + /** + * Sets lock with the current user + * + * @return bool + */ + 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() + ]; + + 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'] ?? []; + + if ( + empty($data) === false && + $data['user'] !== $this->user()->id() && + $user = $this->kirby()->user($data['user']) + ) { + $time = (int)($data['time']); + + return [ + 'user' => $user->id(), + 'email' => $user->email(), + 'time' => $time, + 'unlockable' => ($time + 60) <= time() + ]; + } + + return false; + } + + /** + * 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; + } + + 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'] ?? []; + + return in_array($this->user()->id(), $data) === true; + } + + /** + * Returns the app instance + * + * @return \Kirby\Cms\App + */ + protected function kirby(): App + { + return $this->model->kirby(); + } + + /** + * Removes lock of current user + * + * @return bool + */ + 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 + ]); + } + + // remove lock + unset($this->data['lock']); + + return $this->kirby()->locks()->set($this->model, $this->data); + } + + /** + * 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()] + ); + + 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; + } + + // add lock user to unlocked data + $this->data['unlock'] = $this->data['unlock'] ?? []; + $this->data['unlock'][] = $this->data['lock']['user']; + + // remove lock + unset($this->data['lock']); + + return $this->kirby()->locks()->set($this->model, $this->data); + } + + /** + * Returns currently authenticated user; + * throws exception if none is authenticated + * + * @return \Kirby\Cms\User + */ + protected function user(): User + { + if ($user = $this->kirby()->user()) { + return $user; + } + + throw new PermissionException('No user authenticated.'); + } +} diff --git a/kirby/src/Cms/ContentLocks.php b/kirby/src/Cms/ContentLocks.php new file mode 100755 index 0000000..2e4e73a --- /dev/null +++ b/kirby/src/Cms/ContentLocks.php @@ -0,0 +1,225 @@ +, + * Lukas Bestle + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class ContentLocks +{ + /** + * 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 = []; + + /** + * 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 + */ + protected function closeHandle(string $file) + { + if (isset($this->handles[$file]) === false) { + return; + } + + $handle = $this->handles[$file]; + $result = flock($handle, LOCK_UN) && fclose($handle); + + if ($result !== true) { + throw new Exception('Unexpected file system error.'); // @codeCoverageIgnore + } + + 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 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] ?? []; + } + + // 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 ($filesize > 0) { + // always read the whole file + rewind($handle); + $string = fread($handle, $filesize); + $data = Yaml::decode($string); + } + } + + $this->data[$file] = $data ?? []; + + 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 + */ + 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; + } + + $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 + } + + 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(); + } + + /** + * Sets and writes the lock/unlock data for the specified model + * + * @param \Kirby\Cms\ModelWithContent $model + * @param array $data + * @return bool + */ + 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; + + // 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 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); + + return F::remove($file); + } + + $yaml = Yaml::encode($this->data[$file]); + + // 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 + } + + return true; + } +} diff --git a/kirby/src/Cms/ContentTranslation.php b/kirby/src/Cms/ContentTranslation.php new file mode 100755 index 0000000..e51bfc0 --- /dev/null +++ b/kirby/src/Cms/ContentTranslation.php @@ -0,0 +1,242 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class ContentTranslation +{ + use Properties; + + /** + * @var string + */ + protected $code; + + /** + * @var array + */ + protected $content; + + /** + * @var string + */ + protected $contentFile; + + /** + * @var Model + */ + protected $parent; + + /** + * @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']); + } + + /** + * 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 translation content + * as plain array + * + * @return array + */ + public function content(): array + { + $parent = $this->parent(); + + if ($this->content === null) { + $this->content = $parent->readContent($this->code()); + } + + $content = $this->content; + + // merge with the default content + if ($this->isDefault() === false && $defaultLanguage = $parent->kirby()->defaultLanguage()) { + $default = []; + + if ($defaultTranslation = $parent->translation($defaultLanguage->code())) { + $default = $defaultTranslation->content(); + } + + $content = array_merge($default, $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); + } + + /** + * 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(); + } + + /** + * 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; + } + + /** + * Returns the parent page, file or site object + * + * @return \Kirby\Cms\Model + */ + public function parent() + { + return $this->parent; + } + + /** + * @param string $code + * @return self + */ + protected function setCode(string $code) + { + $this->code = $code; + return $this; + } + + /** + * @param array $content + * @return self + */ + protected function setContent(array $content = null) + { + $this->content = $content; + return $this; + } + + /** + * @param \Kirby\Cms\Model $parent + * @return self + */ + protected function setParent(Model $parent) + { + $this->parent = $parent; + return $this; + } + + /** + * @param string $slug + * @return self + */ + protected function setSlug(string $slug = null) + { + $this->slug = $slug; + return $this; + } + + /** + * Returns the custom translation slug + * + * @return string|null + */ + public function slug(): ?string + { + return $this->slug = $this->slug ?? ($this->content()['slug'] ?? null); + } + + /** + * Merge the old and new data + * + * @param array|null $data + * @param bool $overwrite + * @return self + */ + public function update(array $data = null, bool $overwrite = false) + { + $this->content = $overwrite === true ? (array)$data : array_merge($this->content(), (array)$data); + return $this; + } + + /** + * Converts the most imporant 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/Dir.php b/kirby/src/Cms/Dir.php new file mode 100755 index 0000000..efcd3bd --- /dev/null +++ b/kirby/src/Cms/Dir.php @@ -0,0 +1,180 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Dir extends \Kirby\Toolkit\Dir +{ + public static $numSeparator = '_'; + + /** + * Scans the directory and analyzes files, + * content, meta info and children. This is used + * in Page, Site and User objects to fetch all + * relevant information. + * + * @param string $dir + * @param string $contentExtension + * @param array $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 = Dir::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 = $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; + } +} diff --git a/kirby/src/Cms/Email.php b/kirby/src/Cms/Email.php new file mode 100755 index 0000000..242fa84 --- /dev/null +++ b/kirby/src/Cms/Email.php @@ -0,0 +1,248 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Email +{ + /** + * 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; + + /** + * 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); + + // add transport settings + if (isset($this->props['transport']) === false) { + $this->props['transport'] = $this->options['transport'] ?? []; + } + + // 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(); + } + + /** + * 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 + */ + 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] + ]); + } + + return $this->options['presets'][$preset]; + } + + /** + * Renders the email template(s) and sets the body props + * to the result + * + * @return void + */ + protected function template(): void + { + if (isset($this->props['template']) === true) { + + // 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'); + + if ($html->exists()) { + $this->props['body'] = [ + 'html' => $html->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'); + } + } + } + + /** + * 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; + } + + /** + * 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] ?? []; + + // 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'); + } + } + + 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'); + + $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; + } + + // 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; + } + } + + /** + * 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/Field.php b/kirby/src/Cms/Field.php new file mode 100755 index 0000000..5ce8e7b --- /dev/null +++ b/kirby/src/Cms/Field.php @@ -0,0 +1,257 @@ +myField()->lower(); + * ``` + * + * @package Kirby Cms + * @author Bastian Allgeier + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Field +{ + /** + * Field method aliases + * + * @var array + */ + public static $aliases = []; + + /** + * The field name + * + * @var string + */ + protected $key; + + /** + * 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 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); + + 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::$methods[$method]) === true) { + return static::$methods[$method](clone $this, ...$arguments); + } + } + + return $this; + } + + /** + * Creates a new field object + * + * @param object $parent + * @param string $key + * @param mixed $value + */ + public function __construct($parent = null, string $key, $value) + { + $this->key = $key; + $this->value = $value; + $this->parent = $parent; + } + + /** + * Simplifies the var_dump result + * + * @see Field::toArray + * @return void + */ + 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(); + } + + /** + * 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 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; + } + + /** + * @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 self + */ + public function or($fallback = null) + { + if ($this->isNotEmpty()) { + return $this; + } + + if (is_a($fallback, 'Kirby\Cms\Field') === true) { + return $fallback; + } + + $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; + } + + /** + * 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 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 + */ + 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)); + } + + $clone = clone $this; + $clone->value = $value; + + return $clone; + } +} diff --git a/kirby/src/Cms/File.php b/kirby/src/Cms/File.php new file mode 100755 index 0000000..dc0c804 --- /dev/null +++ b/kirby/src/Cms/File.php @@ -0,0 +1,755 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class File extends ModelWithContent +{ + const CLASS_ALIAS = 'file'; + + use FileActions; + use FileFoundation; + use FileModifications; + use HasMethods; + use HasSiblings; + + /** + * The parent asset object + * This is used to do actual file + * method calls, like size, mime, etc. + * + * @var \Kirby\Image\Image + */ + protected $asset; + + /** + * Cache for the initialized blueprint object + * + * @var \Kirby\Cms\FileBlueprint + */ + protected $blueprint; + + /** + * @var string + */ + protected $id; + + /** + * @var string + */ + protected $filename; + + /** + * 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, $arguments); + } + + /** + * Creates a new File object + * + * @param array $props + */ + public function __construct(array $props) + { + // 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 Image object + * + * @internal + * @return \Kirby\Image\Image + */ + public function asset() + { + return $this->asset = $this->asset ?? new Image($this->root()); + } + + /** + * 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(); + } + + /** + * 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 $type (null|auto|kirbytext|markdown) + * @param bool $absolute + * @return string + */ + public function dragText(string $type = null, bool $absolute = false): string + { + $type = $type ?? 'auto'; + + if ($type === 'auto') { + $type = option('panel.kirbytext', true) ? 'kirbytext' : 'markdown'; + } + + $url = $absolute ? $this->id() : $this->filename(); + + switch ($type) { + case 'markdown': + if ($this->type() === 'image') { + return '![' . $this->alt() . '](' . $url . ')'; + } else { + return '[' . $this->filename() . '](' . $url . ')'; + } + // no break + default: + if ($this->type() === 'image') { + return '(image: ' . $url . ')'; + } else { + return '(file: ' . $url . ')'; + } + } + } + + /** + * Constructs a File object + * + * @internal + * @param mixed $props + * @return self + */ + 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(); + } + + /** + * 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(); + } + + /** + * Create a unique media hash + * + * @internal + * @return string + */ + public function mediaHash(): string + { + return crc32($this->filename()) . '-' . $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(); + } + + /** + * 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(); + } + + /** + * @deprecated 3.0.0 Use `File::content()` instead + * + * @return \Kirby\Cms\Content + */ + public function meta() + { + deprecated('$file->meta() is deprecated, use $file->content() instead. $file->meta() will be removed in Kirby 3.5.0.'); + + return $this->content(); + } + + /** + * Get 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) + { + $file = $this->modifiedFile(); + $content = $this->modifiedContent(); + $modified = max($file, $content); + + if (is_null($format) === true) { + return $modified; + } + + $handler = $handler ?? $this->kirby()->option('date.handler', 'date'); + + return $handler($format, $modified); + } + + /** + * Timestamp of the last modification + * of the content file + * + * @return int + */ + protected function modifiedContent(): int + { + return F::modified($this->contentFile()); + } + + /** + * 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; + } + + /** + * Panel icon definition + * + * @internal + * @param array $params + * @return array + */ + public function panelIcon(array $params = null): array + { + $colorBlue = '#81a2be'; + $colorPurple = '#b294bb'; + $colorOrange = '#de935f'; + $colorGreen = '#a7bd68'; + $colorAqua = '#8abeb7'; + $colorYellow = '#f0c674'; + $colorRed = '#d16464'; + $colorWhite = '#c5c9c6'; + + $types = [ + 'image' => ['color' => $colorOrange, 'type' => 'file-image'], + 'video' => ['color' => $colorYellow, 'type' => 'file-video'], + 'document' => ['color' => $colorRed, 'type' => 'file-document'], + 'audio' => ['color' => $colorAqua, 'type' => 'file-audio'], + 'code' => ['color' => $colorBlue, 'type' => 'file-code'], + 'archive' => ['color' => $colorWhite, 'type' => 'file-zip'], + ]; + + $extensions = [ + 'indd' => ['color' => $colorPurple], + 'xls' => ['color' => $colorGreen, 'type' => 'file-spreadsheet'], + 'xlsx' => ['color' => $colorGreen, 'type' => 'file-spreadsheet'], + 'csv' => ['color' => $colorGreen, 'type' => 'file-spreadsheet'], + 'docx' => ['color' => $colorBlue, 'type' => 'file-word'], + 'doc' => ['color' => $colorBlue, 'type' => 'file-word'], + 'rtf' => ['color' => $colorBlue, 'type' => 'file-word'], + 'mdown' => ['type' => 'file-text'], + 'md' => ['type' => 'file-text'] + ]; + + $definition = array_merge($types[$this->type()] ?? [], $extensions[$this->extension()] ?? []); + + $params['type'] = $definition['type'] ?? 'file'; + $params['color'] = $definition['color'] ?? $colorWhite; + + return parent::panelIcon($params); + } + + /** + * Returns the image file object based on provided query + * + * @internal + * @param string|null $query + * @return \Kirby\Cms\File|\Kirby\Cms\Asset|null + */ + protected function panelImageSource(string $query = null) + { + if ($query === null && $this->isViewable()) { + return $this; + } + + return parent::panelImageSource($query); + } + + /** + * Returns the full path without leading slash + * + * @internal + * @return string + */ + public function panelPath(): string + { + return 'files/' . $this->filename(); + } + + /** + * Prepares the response data for file pickers + * and file fields + * + * @param array|null $params + * @return array + */ + public function panelPickerData(array $params = []): array + { + $image = $this->panelImage($params['image'] ?? []); + $icon = $this->panelIcon($image); + $uuid = $this->id(); + + if (empty($params['model']) === false) { + $uuid = $this->parent() === $params['model'] ? $this->filename() : $this->id(); + $absolute = $this->parent() !== $params['model']; + } + + return [ + 'filename' => $this->filename(), + 'dragText' => $this->dragText('auto', $absolute ?? false), + 'icon' => $icon, + 'id' => $this->id(), + 'image' => $image, + 'info' => $this->toString($params['info'] ?? false), + 'link' => $this->panelUrl(true), + 'text' => $this->toString($params['text'] ?? '{{ file.filename }}'), + 'type' => $this->type(), + 'url' => $this->url(), + 'uuid' => $uuid, + ]; + } + + /** + * Returns the url to the editing view + * in the panel + * + * @internal + * @param bool $relative + * @return string + */ + public function panelUrl(bool $relative = false): string + { + return $this->parent()->panelUrl($relative) . '/' . $this->panelPath(); + } + + /** + * Returns the parent Model object + * + * @return \Kirby\Cms\Model + */ + public function parent() + { + return $this->parent = $this->parent ?? $this->kirby()->site(); + } + + /** + * 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 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->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 self + */ + 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 self + */ + protected function setFilename(string $filename) + { + $this->filename = $filename; + return $this; + } + + /** + * Sets the parent model object + * + * @param \Kirby\Cms\Model $parent + * @return self + */ + protected function setParent(Model $parent = null) + { + $this->parent = $parent; + return $this; + } + + /** + * Always set the root to null, to invoke + * auto root detection + * + * @param string|null $root + * @return self + */ + protected function setRoot(string $root = null) + { + $this->root = null; + return $this; + } + + /** + * @param string $template + * @return self + */ + protected function setTemplate(string $template = null) + { + $this->template = $template; + return $this; + } + + /** + * Sets the url + * + * @param string $url + * @return self + */ + 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->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)->filterBy('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->url = $this->kirby()->component('file::url')($this->kirby(), $this); + } +} diff --git a/kirby/src/Cms/FileActions.php b/kirby/src/Cms/FileActions.php new file mode 100755 index 0000000..bcbe373 --- /dev/null +++ b/kirby/src/Cms/FileActions.php @@ -0,0 +1,301 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait FileActions +{ + /** + * Renames the file without touching the extension + * The store is used to actually execute this. + * + * @param string $name + * @param bool $sanitize + * @return self + */ + 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; + } + + return $this->commit('changeName', [$this, $name], function ($oldFile, $name) { + $newFile = $oldFile->clone([ + 'filename' => $name . '.' . $oldFile->extension(), + ]); + + if ($oldFile->exists() === false) { + return $newFile; + } + + 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 all public versions + $oldFile->unpublish(); + + // rename the main file + F::move($oldFile->root(), $newFile->root()); + + 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()); + } + + + return $newFile; + }); + } + + /** + * Changes the file's sorting number in the meta file + * + * @param int $sort + * @return self + */ + public function changeSort(int $sort) + { + return $this->commit('changeSort', [$this, $sort], function ($file, $sort) { + return $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(); + + $this->rules()->$action(...$arguments); + $kirby->trigger('file.' . $action . ':before', ...$arguments); + $result = $callback(...$arguments); + $kirby->trigger('file.' . $action . ':after', $result, $old); + $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()); + + 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()); + } + + /** + * 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 self + */ + 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'])); + + $props['model'] = strtolower($props['template'] ?? 'default'); + + // create the basic file and a test upload object + $file = File::factory($props); + $upload = new Image($props['source']); + + // create a form for the file + $form = Form::for($file, [ + 'values' => $props['content'] ?? [] + ]); + + // inject the content + $file = $file->clone(['content' => $form->strings(true)]); + + // run the hook + return $file->commit('create', [$file, $upload], function ($file, $upload) { + + // 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'); + } + + // 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); + + // add the file to the list of siblings + $file->siblings()->append($file->id(), $file); + + // 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', [$this], function ($file) { + + // remove all versions in the media folder + $file->unpublish(); + + // remove the lock of the old file + if ($lock = $file->lock()) { + $lock->remove(); + } + + if ($file->kirby()->multilang() === true) { + foreach ($file->translations() as $translation) { + F::remove($file->contentFile($translation->code())); + } + } else { + F::remove($file->contentFile()); + } + + F::remove($file->root()); + + return true; + }); + } + + /** + * Move the file to the public media folder + * if it's not already there. + * + * @return self + */ + public function publish() + { + Media::publish($this->root(), $this->mediaRoot()); + return $this; + } + + /** + * @deprecated 3.0.0 Use `File::changeName()` instead + * + * @param string $name + * @param bool $sanitize + * @return self + */ + public function rename(string $name, bool $sanitize = true) + { + deprecated('$file->rename() is deprecated, use $file->changeName() instead. $file->rename() will be removed in Kirby 3.5.0.'); + + return $this->changeName($name, $sanitize); + } + + /** + * 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 self + */ + public function replace(string $source) + { + return $this->commit('replace', [$this, new Image($source)], function ($file, $upload) { + + // 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 self + */ + public function unpublish() + { + Media::unpublish($this->parent()->mediaRoot(), $this->filename()); + return $this; + } +} diff --git a/kirby/src/Cms/FileBlueprint.php b/kirby/src/Cms/FileBlueprint.php new file mode 100755 index 0000000..2af1e4c --- /dev/null +++ b/kirby/src/Cms/FileBlueprint.php @@ -0,0 +1,78 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class FileBlueprint extends Blueprint +{ + 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, + 'replace' => null, + 'update' => null, + ] + ); + + // normalize the accept settings + $this->props['accept'] = $this->normalizeAccept($this->props['accept'] ?? []); + } + + /** + * @return array + */ + public function accept(): array + { + return $this->props['accept']; + } + + /** + * @param mixed $accept + * @return array + */ + protected function normalizeAccept($accept = null): array + { + if (is_string($accept) === true) { + $accept = [ + 'mime' => $accept + ]; + } + + // accept anything + if (empty($accept) === true) { + return []; + } + + $accept = array_change_key_case($accept); + + $defaults = [ + 'mime' => null, + 'maxheight' => null, + 'maxsize' => null, + 'maxwidth' => null, + 'minheight' => null, + 'minsize' => null, + 'minwidth' => null, + 'orientation' => null + ]; + + return array_merge($defaults, $accept); + } +} diff --git a/kirby/src/Cms/FileFoundation.php b/kirby/src/Cms/FileFoundation.php new file mode 100755 index 0000000..cc1b4f9 --- /dev/null +++ b/kirby/src/Cms/FileFoundation.php @@ -0,0 +1,247 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait FileFoundation +{ + protected $asset; + protected $root; + protected $url; + + /** + * Magic caller for asset methods + * + * @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); + } + + throw new BadMethodCallException('The method: "' . $method . '" does not exist'); + } + + /** + * Constructor sets all file properties + * + * @param array $props + */ + public function __construct(array $props) + { + $this->setProperties($props); + } + + /** + * Converts the file object to a string + * In case of an image, it will create an image tag + * Otherwise it will return the url + * + * @return string + */ + public function __toString(): string + { + if ($this->type() === 'image') { + return $this->html(); + } + + return $this->url(); + } + + /** + * Returns the Image object + * + * @return \Kirby\Image\Image + */ + public function asset() + { + return $this->asset = $this->asset ?? new Image($this->root()); + } + + /** + * Checks if the file exists on disk + * + * @return bool + */ + public function exists(): bool + { + return file_exists($this->root()) === true; + } + + /** + * Returns the file extension + * + * @return string + */ + public function extension(): string + { + return F::extension($this->root()); + } + + /** + * Converts the file to html + * + * @param array $attr + * @return string + */ + public function html(array $attr = []): string + { + if ($this->type() === 'image') { + return Html::img($this->url(), array_merge(['alt' => $this->alt()], $attr)); + } else { + return Html::a($this->url(), $attr); + } + } + + /** + * Checks if the file is a resizable image + * + * @return bool + */ + public function isResizable(): bool + { + $resizable = [ + 'jpg', + 'jpeg', + 'gif', + 'png', + 'webp' + ]; + + return in_array($this->extension(), $resizable) === true; + } + + /** + * Checks if a preview can be displayed for the file + * in the panel or in the frontend + * + * @return bool + */ + public function isViewable(): bool + { + $viewable = [ + 'jpg', + 'jpeg', + 'gif', + 'png', + 'svg', + 'webp' + ]; + + return in_array($this->extension(), $viewable) === true; + } + + /** + * Returns the app instance + * + * @return \Kirby\Cms\App + */ + public function kirby() + { + return App::instance(); + } + + /** + * Get 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) + { + return F::modified($this->root(), $format, $handler ?? $this->kirby()->option('date.handler', 'date')); + } + + /** + * Returns the absolute path to the file root + * + * @return string|null + */ + public function root(): ?string + { + return $this->root; + } + + /** + * Setter for the root + * + * @param string $root + * @return self + */ + protected function setRoot(string $root = null) + { + $this->root = $root; + return $this; + } + + /** + * Setter for the file url + * + * @param string $url + * @return self + */ + protected function setUrl(string $url) + { + $this->url = $url; + return $this; + } + + /** + * Convert the object to an array + * + * @return array + */ + public function toArray(): array + { + $array = array_merge($this->asset()->toArray(), [ + 'isResizable' => $this->isResizable(), + 'url' => $this->url(), + ]); + + ksort($array); + + return $array; + } + + /** + * Returns the file type + * + * @return string|null + */ + public function type(): ?string + { + return F::type($this->root()); + } + + /** + * Returns the absolute url for the file + * + * @return string + */ + public function url(): string + { + return $this->url; + } +} diff --git a/kirby/src/Cms/FileModifications.php b/kirby/src/Cms/FileModifications.php new file mode 100755 index 0000000..51e3962 --- /dev/null +++ b/kirby/src/Cms/FileModifications.php @@ -0,0 +1,200 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +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]); + } + + /** + * 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 $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; + } + + 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 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]); + } + + /** + * Resizes the file with the given width and height + * while keeping the aspect ratio. + * + * @param int $width + * @param int $height + * @param int $quality + * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File + */ + 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 $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_array($sizes) === false || empty($sizes) === true) { + return null; + } + + $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'; + } + + $set[] = $this->thumb($options)->url() . ' ' . $condition; + } + + 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 + */ + 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; + } + + $result = $this->kirby()->component('file::version')($this->kirby(), $this, $options); + + if (is_a($result, 'Kirby\Cms\FileVersion') === false && is_a($result, 'Kirby\Cms\File') === false) { + throw new InvalidArgumentException('The file::version component must return a File or FileVersion object'); + } + + return $result; + } +} diff --git a/kirby/src/Cms/FilePermissions.php b/kirby/src/Cms/FilePermissions.php new file mode 100755 index 0000000..91fca71 --- /dev/null +++ b/kirby/src/Cms/FilePermissions.php @@ -0,0 +1,17 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class FilePermissions extends ModelPermissions +{ + protected $category = 'files'; +} diff --git a/kirby/src/Cms/FilePicker.php b/kirby/src/Cms/FilePicker.php new file mode 100755 index 0000000..b09c09f --- /dev/null +++ b/kirby/src/Cms/FilePicker.php @@ -0,0 +1,73 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class FilePicker extends Picker +{ + /** + * Extends the basic defaults + * + * @return array + */ + public function defaults(): array + { + $defaults = parent::defaults(); + $defaults['text'] = '{{ file.filename }}'; + + return $defaults; + } + + /** + * Search all files for the picker + * + * @return \Kirby\Cms\Files|null + */ + 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'; + } + + // 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'); + } + + // search + $files = $this->search($files); + + // paginate + return $this->paginate($files); + } +} diff --git a/kirby/src/Cms/FileRules.php b/kirby/src/Cms/FileRules.php new file mode 100755 index 0000000..60e943b --- /dev/null +++ b/kirby/src/Cms/FileRules.php @@ -0,0 +1,201 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class FileRules +{ + 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()] + ]); + } + + $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()] + ]); + } + + return true; + } + + public static function changeSort(File $file, int $sort): bool + { + return true; + } + + public static function create(File $file, Image $upload): bool + { + if ($file->exists() === true) { + throw new LogicException('The file exists and cannot be overwritten'); + } + + if ($file->permissions()->create() !== true) { + throw new PermissionException('The file cannot be created'); + } + + static::validExtension($file, $file->extension()); + static::validMime($file, $upload->mime()); + static::validFilename($file, $file->filename()); + + $upload->match($file->blueprint()->accept()); + + return true; + } + + public static function delete(File $file): bool + { + if ($file->permissions()->delete() !== true) { + throw new PermissionException('The file cannot be deleted'); + } + + return true; + } + + public static function replace(File $file, Image $upload): bool + { + if ($file->permissions()->replace() !== true) { + throw new PermissionException('The file cannot be replaced'); + } + + static::validMime($file, $upload->mime()); + + + if ( + (string)$upload->mime() !== (string)$file->mime() && + (string)$upload->extension() !== (string)$file->extension() + ) { + throw new InvalidArgumentException([ + 'key' => 'file.mime.differs', + 'data' => ['mime' => $file->mime()] + ]); + } + + $upload->match($file->blueprint()->accept()); + + return true; + } + + public static function update(File $file, array $content = []): bool + { + if ($file->permissions()->update() !== true) { + throw new PermissionException('The file cannot be updated'); + } + + return true; + } + + public static function validExtension(File $file, string $extension): bool + { + // make it easier to compare the extension + $extension = strtolower($extension); + + if (empty($extension)) { + throw new InvalidArgumentException([ + 'key' => 'file.extension.missing', + 'data' => ['filename' => $file->filename()] + ]); + } + + if (V::in($extension, ['php', 'html', 'htm', 'exe', App::instance()->contentExtension()])) { + throw new InvalidArgumentException([ + 'key' => 'file.extension.forbidden', + 'data' => ['extension' => $extension] + ]); + } + + if (Str::contains($extension, 'php')) { + throw new InvalidArgumentException([ + 'key' => 'file.type.forbidden', + 'data' => ['type' => 'PHP'] + ]); + } + + return true; + } + + public static function validFilename(File $file, string $filename) + { + + // make it easier to compare the filename + $filename = strtolower($filename); + + // check for missing filenames + if (empty($filename)) { + throw new InvalidArgumentException([ + 'key' => 'file.name.missing' + ]); + } + + // Block htaccess files + if (Str::startsWith($filename, '.ht')) { + throw new InvalidArgumentException([ + 'key' => 'file.type.forbidden', + 'data' => ['type' => 'Apache config'] + ]); + } + + // Block invisible files + if (Str::startsWith($filename, '.')) { + throw new InvalidArgumentException([ + 'key' => 'file.type.forbidden', + 'data' => ['type' => 'invisible'] + ]); + } + + return true; + } + + public static function validMime(File $file, string $mime = null) + { + // make it easier to compare the mime + $mime = strtolower($mime); + + if (empty($mime)) { + throw new InvalidArgumentException([ + 'key' => 'file.mime.missing', + 'data' => ['filename' => $file->filename()] + ]); + } + + 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 new file mode 100755 index 0000000..2bdb6f9 --- /dev/null +++ b/kirby/src/Cms/FileVersion.php @@ -0,0 +1,102 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class FileVersion +{ + use FileFoundation { + toArray as parentToArray; + } + use Properties; + + protected $modifications; + protected $original; + + 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(); + } + + return $this->asset()->$method(...$arguments); + } + + if (is_a($this->original(), 'Kirby\Cms\File') === true) { + // content fields + return $this->original()->content()->get($method, $arguments); + } + } + + public function id(): string + { + return dirname($this->original()->id()) . '/' . $this->filename(); + } + + /** + * @return \Kirby\Cms\App + */ + public function kirby() + { + return $this->original()->kirby(); + } + + public function modifications(): array + { + return $this->modifications ?? []; + } + + public function original() + { + return $this->original; + } + + public function save() + { + $this->kirby()->thumb($this->original()->root(), $this->root(), $this->modifications()); + return $this; + } + + protected function setModifications(array $modifications = null) + { + $this->modifications = $modifications; + } + + protected function setOriginal($original) + { + $this->original = $original; + } + + /** + * Convert the object to an array + * + * @return array + */ + public function toArray(): array + { + $array = array_merge($this->parentToArray(), [ + 'modifications' => $this->modifications(), + ]); + + ksort($array); + + return $array; + } +} diff --git a/kirby/src/Cms/Filename.php b/kirby/src/Cms/Filename.php new file mode 100755 index 0000000..d4fa708 --- /dev/null +++ b/kirby/src/Cms/Filename.php @@ -0,0 +1,303 @@ + 'top left', + * 'width' => 300, + * 'height' => 200 + * 'quality' => 80 + * ]); + * + * echo $filename->toString(); + * // result: some-file-300x200-crop-top-left-q80.jpg + * + * @package Kirby Cms + * @author Bastian Allgeier + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Filename +{ + /** + * List of all applicable attributes + * + * @var array + */ + protected $attributes; + + /** + * The sanitized file extension + * + * @var string + */ + protected $extension; + + /** + * The source original filename + * + * @var string + */ + protected $filename; + + /** + * The sanitized file name + * + * @var string + */ + protected $name; + + /** + * 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(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 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, function ($item) { + return $item !== null && $item !== false && $item !== ''; + }); + + return $array; + } + + /** + * Converts all processed attributes + * to a string, that can be used in the + * new filename + * + * @param string $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 = ''; + } + + switch ($key) { + case 'dimensions': + $result[] = $value; + break; + case 'crop': + $result[] = ($value === 'center') ? null : $key . '-' . $value; + break; + default: + $result[] = $key . $value; + } + } + + $result = array_filter($result); + $attributes = implode('-', $result); + + if (empty($attributes) === true) { + return ''; + } + + return $prefix . $attributes; + } + + /** + * Normalizes the blur option value + * + * @return false|int + */ + public function blur() + { + $value = $this->attributes['blur'] ?? false; + + if ($value === false) { + return false; + } + + return (int)$value; + } + + /** + * 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; + } + + 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 []; + } + + return [ + 'width' => $this->attributes['width'] ?? null, + 'height' => $this->attributes['height'] ?? null + ]; + } + + /** + * 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; + + // 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; + } + + /** + * Normalizes the quality option value + * + * @return false|int + */ + public function quality() + { + $value = $this->attributes['quality'] ?? false; + + if ($value === false || $value === true) { + return false; + } + + 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 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() + ]); + } +} diff --git a/kirby/src/Cms/Files.php b/kirby/src/Cms/Files.php new file mode 100755 index 0000000..ce32bf9 --- /dev/null +++ b/kirby/src/Cms/Files.php @@ -0,0 +1,138 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Files extends Collection +{ + /** + * All registered files methods + * + * @var array + */ + public static $methods = []; + + /** + * Adds a single file or + * an entire second collection to the + * current collection + * + * @param mixed $object + * @return self + */ + public function add($object) + { + // add a page collection + if (is_a($object, static::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 object + } elseif (is_a($object, 'Kirby\Cms\File') === true) { + $this->__set($object->id(), $object); + } + + return $this; + } + + /** + * Sort all given files by the + * order in the array + * + * @param array $files List of filenames + * @param int $offset Sorting offset + * @return self + */ + public function changeSort(array $files, int $offset = 0) + { + foreach ($files as $filename) { + if ($file = $this->get($filename)) { + $offset++; + $file->changeSort($offset); + } + } + + return $this; + } + + /** + * Creates a files collection from an array of props + * + * @param array $files + * @param \Kirby\Cms\Model $parent + * @param array $inject + * @return self + */ + 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; + + $file = File::factory($props); + + $collection->data[$file->id()] = $file; + } + + 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, '/')); + } + + /** + * 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); + } + + /** + * Filter all files by the given template + * + * @param null|string|array $template + * @return self + */ + public function template($template) + { + if (empty($template) === true) { + return $this; + } + + return $this->filterBy('template', is_array($template) ? 'in' : '==', $template); + } +} diff --git a/kirby/src/Cms/Form.php b/kirby/src/Cms/Form.php new file mode 100755 index 0000000..30e72fc --- /dev/null +++ b/kirby/src/Cms/Form.php @@ -0,0 +1,87 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Form extends BaseForm +{ + protected $errors; + protected $fields; + protected $values = []; + + public function __construct(array $props) + { + $kirby = App::instance(); + + if ($kirby->multilang() === true) { + $fields = $props['fields'] ?? []; + $languageCode = $props['language'] ?? $kirby->language()->code(); + $isDefaultLanguage = $languageCode === $kirby->defaultLanguage()->code(); + + foreach ($fields as $fieldName => $fieldProps) { + // switch untranslatable fields to readonly + if (($fieldProps['translate'] ?? true) === false && $isDefaultLanguage === false) { + $fields[$fieldName]['unset'] = true; + $fields[$fieldName]['disabled'] = true; + } + } + + $props['fields'] = $fields; + } + + parent::__construct($props); + } + + /** + * @param \Kirby\Cms\Model $model + * @param array $props + * @return self + */ + public static function for(Model $model, array $props = []) + { + // get the original model data + $original = $model->content($props['language'] ?? null)->toArray(); + $values = $props['values'] ?? []; + + // convert closures to values + foreach ($values as $key => $value) { + if (is_a($value, 'Closure') === true) { + $values[$key] = $value($original[$key] ?? null); + } + } + + // set a few defaults + $props['values'] = array_merge($original, $values); + $props['fields'] = $props['fields'] ?? []; + $props['model'] = $model; + + // search for the blueprint + if (method_exists($model, 'blueprint') === true && $blueprint = $model->blueprint()) { + $props['fields'] = $blueprint->fields(); + } + + $ignoreDisabled = $props['ignoreDisabled'] ?? false; + + // REFACTOR: this could be more elegant + if ($ignoreDisabled === true) { + $props['fields'] = array_map(function ($field) { + $field['disabled'] = false; + return $field; + }, $props['fields']); + } + + return new static($props); + } +} diff --git a/kirby/src/Cms/HasChildren.php b/kirby/src/Cms/HasChildren.php new file mode 100755 index 0000000..dbdb74f --- /dev/null +++ b/kirby/src/Cms/HasChildren.php @@ -0,0 +1,263 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait HasChildren +{ + /** + * The Pages collection + * + * @var \Kirby\Cms\Pages + */ + public $children; + + /** + * The list of available drafts + * + * @var \Kirby\Cms\Pages + */ + public $drafts; + + /** + * Returns the Pages collection + * + * @return \Kirby\Cms\Pages + */ + public function children() + { + if (is_a($this->children, 'Kirby\Cms\Pages') === true) { + return $this->children; + } + + return $this->children = Pages::factory($this->inventory()['children'], $this); + } + + /** + * Returns all children and drafts at the same time + * + * @return \Kirby\Cms\Pages + */ + public function childrenAndDrafts() + { + return $this->children()->merge($this->drafts()); + } + + /** + * Return a list of ids for the model's + * toArray method + * + * @return array + */ + protected function convertChildrenToArray(): array + { + return $this->children()->keys(); + } + + /** + * Searches for a child draft by id + * + * @param string $path + * @return \Kirby\Cms\Page|null + */ + public function draft(string $path) + { + $path = str_replace('_drafts/', '', $path); + + if (Str::contains($path, '/') === false) { + return $this->drafts()->find($path); + } + + $parts = explode('/', $path); + $parent = $this; + + foreach ($parts as $slug) { + if ($page = $parent->find($slug)) { + $parent = $page; + continue; + } + + if ($draft = $parent->drafts()->find($slug)) { + $parent = $draft; + continue; + } + + return null; + } + + return $parent; + } + + /** + * Return all drafts of the model + * + * @return \Kirby\Cms\Pages + */ + public function drafts() + { + if (is_a($this->drafts, 'Kirby\Cms\Pages') === true) { + return $this->drafts; + } + + $kirby = $this->kirby(); + + // create the inventory for all drafts + $inventory = Dir::inventory( + $this->root() . '/_drafts', + $kirby->contentExtension(), + $kirby->contentIgnore(), + $kirby->multilang() + ); + + return $this->drafts = Pages::factory($inventory['children'], $this, true); + } + + /** + * Finds one or multiple children by id + * + * @param string ...$arguments + * @return \Kirby\Cms\Page|\Kirby\Cms\Pages + */ + public function find(...$arguments) + { + return $this->children()->find(...$arguments); + } + + /** + * Finds a single page or draft + * + * @param string $path + * @return \Kirby\Cms\Page|null + */ + public function findPageOrDraft(string $path) + { + return $this->children()->find($path) ?? $this->drafts()->find($path); + } + + /** + * Returns a collection of all children of children + * + * @return \Kirby\Cms\Pages + */ + public function grandChildren() + { + return $this->children()->children(); + } + + /** + * Checks if the model has any children + * + * @return bool + */ + public function hasChildren(): bool + { + return $this->children()->count() > 0; + } + + /** + * Checks if the model has any drafts + * + * @return bool + */ + public function hasDrafts(): bool + { + return $this->drafts()->count() > 0; + } + + /** + * @deprecated 3.0.0 Use `Page::hasUnlistedChildren()` instead + * @return bool + */ + public function hasInvisibleChildren(): bool + { + deprecated('$page->hasInvisibleChildren() is deprecated, use $page->hasUnlistedChildren() instead. $page->hasInvisibleChildren() will be removed in Kirby 3.5.0.'); + + return $this->hasUnlistedChildren(); + } + + /** + * Checks if the page has any listed children + * + * @return bool + */ + public function hasListedChildren(): bool + { + return $this->children()->listed()->count() > 0; + } + + /** + * Checks if the page has any unlisted children + * + * @return bool + */ + public function hasUnlistedChildren(): bool + { + return $this->children()->unlisted()->count() > 0; + } + + /** + * @deprecated 3.0.0 Use `Page::hasListedChildren()` instead + * @return bool + */ + public function hasVisibleChildren(): bool + { + deprecated('$page->hasVisibleChildren() is deprecated, use $page->hasListedChildren() instead. $page->hasVisibleChildren() will be removed in Kirby 3.5.0.'); + + return $this->hasListedChildren(); + } + + /** + * Creates a flat child index + * + * @param bool $drafts + * @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 Children collection + * + * @param array|null $children + * @return self + */ + protected function setChildren(array $children = null) + { + if ($children !== null) { + $this->children = Pages::factory($children, $this); + } + + return $this; + } + + /** + * Sets the Drafts collection + * + * @param array|null $drafts + * @return self + */ + 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 new file mode 100755 index 0000000..9156a47 --- /dev/null +++ b/kirby/src/Cms/HasFiles.php @@ -0,0 +1,226 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait HasFiles +{ + /** + * 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()->filterBy('type', '==', 'audio'); + } + + /** + * Filters the Files collection by type code + * + * @return \Kirby\Cms\Files + */ + public function code() + { + return $this->files()->filterBy('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(); + } + + /** + * 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); + } + + /** + * Filters the Files collection by type documents + * + * @return \Kirby\Cms\Files + */ + public function documents() + { + return $this->files()->filterBy('type', '==', 'document'); + } + + /** + * Returns a specific file by filename or the first one + * + * @param string $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 ($page = $this->find($path)) { + return $page->$in()->find($filename); + } + + return null; + } + + 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; + } + + 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 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 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 videos + * + * @return bool + */ + public function hasVideos(): bool + { + return $this->videos()->count() > 0; + } + + /** + * Returns a specific image by filename or the first one + * + * @param string $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()->filterBy('type', '==', 'image'); + } + + /** + * Sets the Files collection + * + * @param \Kirby\Cms\Files|null $files + * @return self + */ + protected function setFiles(array $files = null) + { + if ($files !== null) { + $this->files = Files::factory($files, $this); + } + + return $this; + } + + /** + * Filters the Files collection by type videos + * + * @return \Kirby\Cms\Files + */ + public function videos() + { + return $this->files()->filterBy('type', '==', 'video'); + } +} diff --git a/kirby/src/Cms/HasMethods.php b/kirby/src/Cms/HasMethods.php new file mode 100755 index 0000000..4a1a6bc --- /dev/null +++ b/kirby/src/Cms/HasMethods.php @@ -0,0 +1,48 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait HasMethods +{ + /** + * 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 + */ + public function callMethod(string $method, array $args = []) + { + return static::$methods[$method]->call($this, ...$args); + } + + /** + * Checks if the object has a registered method + * + * @internal + * @param string $method + * @return bool + */ + public function hasMethod(string $method): bool + { + return isset(static::$methods[$method]) === true; + } +} diff --git a/kirby/src/Cms/HasSiblings.php b/kirby/src/Cms/HasSiblings.php new file mode 100755 index 0000000..78ad1d7 --- /dev/null +++ b/kirby/src/Cms/HasSiblings.php @@ -0,0 +1,134 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait HasSiblings +{ + /** + * Returns the position / index in the collection + * + * @return int + */ + public function indexOf(): int + { + return $this->siblingsCollection()->indexOf($this); + } + + /** + * Returns the next item in the collection if available + * + * @return \Kirby\Cms\Model|null + */ + public function next() + { + return $this->siblingsCollection()->nth($this->indexOf() + 1); + } + + /** + * Returns the end of the collection starting after the current item + * + * @return \Kirby\Cms\Collection + */ + public function nextAll() + { + return $this->siblingsCollection()->slice($this->indexOf() + 1); + } + + /** + * Returns the previous item in the collection if available + * + * @return \Kirby\Cms\Model|null + */ + public function prev() + { + return $this->siblingsCollection()->nth($this->indexOf() - 1); + } + + /** + * Returns the beginning of the collection before the current item + * + * @return \Kirby\Cms\Collection + */ + public function prevAll() + { + return $this->siblingsCollection()->slice(0, $this->indexOf()); + } + + /** + * 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); + } + + return $siblings; + } + + /** + * Checks if there's a next item in the collection + * + * @return bool + */ + public function hasNext(): bool + { + return $this->next() !== null; + } + + /** + * Checks if there's a previous item in the collection + * + * @return bool + */ + public function hasPrev(): bool + { + return $this->prev() !== null; + } + + /** + * Checks if the item is the first in the collection + * + * @return bool + */ + public function isFirst(): bool + { + return $this->siblingsCollection()->first()->is($this); + } + + /** + * Checks if the item is the last in the collection + * + * @return bool + */ + public function isLast(): bool + { + return $this->siblingsCollection()->last()->is($this); + } + + /** + * Checks if the item is at a certain position + * + * @param int $n + * @return bool + */ + public function isNth(int $n): bool + { + return $this->indexOf() === $n; + } +} diff --git a/kirby/src/Cms/Html.php b/kirby/src/Cms/Html.php new file mode 100755 index 0000000..c0ded7e --- /dev/null +++ b/kirby/src/Cms/Html.php @@ -0,0 +1,30 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Html extends \Kirby\Toolkit\Html +{ + /** + * Generates an `a` tag with an absolute Url + * + * @param string $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); + } +} diff --git a/kirby/src/Cms/Ingredients.php b/kirby/src/Cms/Ingredients.php new file mode 100755 index 0000000..64fd9d2 --- /dev/null +++ b/kirby/src/Cms/Ingredients.php @@ -0,0 +1,95 @@ +urls()` and `$kirby->roots()` objects. + * Those are configured in `kirby/config/urls.php` + * and `kirby/config/roots.php` + * + * @package Kirby Cms + * @author Bastian Allgeier + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Ingredients +{ + /** + * @var array + */ + protected $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 $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; + } + + /** + * 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 self + */ + 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); + } + + /** + * Returns all ingredients as plain array + * + * @return array + */ + public function toArray(): array + { + return $this->ingredients; + } +} diff --git a/kirby/src/Cms/KirbyTag.php b/kirby/src/Cms/KirbyTag.php new file mode 100755 index 0000000..86fade3 --- /dev/null +++ b/kirby/src/Cms/KirbyTag.php @@ -0,0 +1,60 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class KirbyTag extends \Kirby\Text\KirbyTag +{ + /** + * Finds a file for the given path. + * The method first searches the file + * in the current parent, if it's a page. + * Afterwards it uses Kirby's global file finder. + * + * @param string $path + * @return \Kirby\Cms\File|null + */ + public function file(string $path) + { + $parent = $this->parent(); + + if (method_exists($parent, 'file') === true && $file = $parent->file($path)) { + return $file; + } + + if (is_a($parent, 'Kirby\Cms\File') === true && $file = $parent->page()->file($path)) { + return $file; + } + + return $this->kirby()->file($path, null, true); + } + + /** + * Returns the current Kirby instance + * + * @return \Kirby\Cms\App + */ + public function kirby() + { + return $this->data['kirby'] ?? App::instance(); + } + + /** + * Returns the parent model + * + * @return \Kirby\Cms\Model|null + */ + public function parent() + { + return $this->data['parent']; + } +} diff --git a/kirby/src/Cms/KirbyTags.php b/kirby/src/Cms/KirbyTags.php new file mode 100755 index 0000000..6442fa2 --- /dev/null +++ b/kirby/src/Cms/KirbyTags.php @@ -0,0 +1,58 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class KirbyTags extends \Kirby\Text\KirbyTags +{ + /** + * The KirbyTag rendering class + * + * @var string + */ + protected static $tagClass = 'Kirby\Cms\KirbyTag'; + + /** + * @param string $text + * @param array $data + * @param array $options + * @param array $hooks + * @return string + */ + public static function parse(string $text = null, array $data = [], array $options = [], array $hooks = []): string + { + $text = static::hooks($hooks['kirbytags:before'] ?? [], $text, $data, $options); + $text = parent::parse($text, $data, $options); + $text = static::hooks($hooks['kirbytags:after'] ?? [], $text, $data, $options); + + return $text; + } + + /** + * Runs the given hooks and returns the + * modified text + * + * @param array $hooks + * @param string $text + * @param array $data + * @param array $options + * @return string|null + */ + protected static function hooks(array $hooks, string $text = null, array $data, array $options): ?string + { + foreach ($hooks as $hook) { + $text = $hook->call($data['kirby'], $text, $data, $options); + } + + return $text; + } +} diff --git a/kirby/src/Cms/Language.php b/kirby/src/Cms/Language.php new file mode 100755 index 0000000..1404524 --- /dev/null +++ b/kirby/src/Cms/Language.php @@ -0,0 +1,669 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +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 self + */ + 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()); + } + + return $language; + } + + /** + * Delete the current language and + * all its translation files + * + * @internal + * @return bool + */ + public function delete(): bool + { + if ($this->exists() === false) { + return true; + } + + $kirby = App::instance(); + $languages = $kirby->languages(); + $code = $this->code(); + + if (F::remove($this->root()) !== true) { + throw new Exception('The language could not be deleted'); + } + + if ($languages->count() === 1) { + return $this->converter($code, ''); + } else { + return $this->deleteContentFiles($code); + } + } + + /** + * 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; + } + + /** + * 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); + $code = Str::contains($code, '.') ? Str::before($code, '.') : $code; + $file = $this->kirby()->root('i18n:rules') . '/' . $code . '.json'; + + if (F::exists($file) === false) { + $file = $this->kirby()->root('i18n:rules') . '/' . Str::before($code, '_') . '.json'; + } + + try { + $data = Data::read($file); + } catch (\Exception $e) { + $data = []; + } + + return array_merge($data, $this->slugs()); + } + + /** + * Saves the language settings in the languages folder + * + * @internal + * @return self + */ + public function save() + { + try { + $existingData = Data::read($this->root()); + } catch (Throwable $e) { + $existingData = []; + } + + $props = [ + 'code' => $this->code(), + 'default' => $this->isDefault(), + 'direction' => $this->direction(), + 'locale' => $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 self + */ + protected function setCode(string $code) + { + $this->code = trim($code); + return $this; + } + + /** + * @param bool $default + * @return self + */ + protected function setDefault(bool $default = false) + { + $this->default = $default; + return $this; + } + + /** + * @param string $direction + * @return self + */ + protected function setDirection(string $direction = 'ltr') + { + $this->direction = $direction === 'rtl' ? 'rtl' : 'ltr'; + return $this; + } + + /** + * @param string|array $locale + * @return self + */ + protected function setLocale($locale = null) + { + if (is_array($locale)) { + $this->locale = $locale; + } elseif (is_string($locale)) { + $this->locale = [LC_ALL => $locale]; + } elseif ($locale === null) { + $this->locale = [LC_ALL => $this->code]; + } else { + throw new InvalidArgumentException('Locale must be string or array'); + } + + return $this; + } + + /** + * @param string $name + * @return self + */ + protected function setName(string $name = null) + { + $this->name = trim($name ?? $this->code); + return $this; + } + + /** + * @param array $slugs + * @return self + */ + protected function setSlugs(array $slugs = null) + { + $this->slugs = $slugs ?? []; + return $this; + } + + /** + * @param array $smartypants + * @return self + */ + protected function setSmartypants(array $smartypants = null) + { + $this->smartypants = $smartypants ?? []; + return $this; + } + + /** + * @param array $translations + * @return self + */ + protected function setTranslations(array $translations = null) + { + $this->translations = $translations ?? []; + return $this; + } + + /** + * @param string $url + * @return self + */ + 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 self + */ + public function update(array $props = null) + { + $props['slug'] = Str::slug($props['slug'] ?? null); + $kirby = App::instance(); + $updated = $this->clone($props); + + // 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'); + } + + return $updated->save(); + } +} diff --git a/kirby/src/Cms/LanguageRouter.php b/kirby/src/Cms/LanguageRouter.php new file mode 100755 index 0000000..383e530 --- /dev/null +++ b/kirby/src/Cms/LanguageRouter.php @@ -0,0 +1,131 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class LanguageRouter +{ + /** + * The parent language + * + * @var Language + */ + protected $language; + + /** + * 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; + } + + /** + * Fetches all scoped routes for the + * current language from the Kirby instance + * + * @return array + */ + 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) { + + // no language scope + if (empty($route['language']) === true) { + return false; + } + + // wildcard + if ($route['language'] === '*') { + return true; + } + + // get all applicable languages + $languages = Str::split(strtolower($route['language']), '|'); + + // 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']); + + // prefix all patterns with the page slug + $patterns = array_map(function ($pattern) use ($page, $language) { + return $page->uri($language) . '/' . $pattern; + }, $patterns); + + // reinject 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'); + } + } + } + + return $routes; + } + + /** + * 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 ($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()); + } + } +} diff --git a/kirby/src/Cms/LanguageRoutes.php b/kirby/src/Cms/LanguageRoutes.php new file mode 100755 index 0000000..7d4e3ee --- /dev/null +++ b/kirby/src/Cms/LanguageRoutes.php @@ -0,0 +1,143 @@ +url(); + + foreach ($kirby->languages() as $language) { + + // 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; + } + + // jump through to the fallback if nothing + // can be found for this language + $this->next(); + } + ]; + } + + $routes[] = static::fallback($kirby); + + 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) { + + // 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 + ]); + + if ($url->toString() !== $page->url()) { + return $kirby + ->response() + ->redirect($page->url()); + } + } + + return $kirby->defaultLanguage()->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()->filterBy('baseurl', $kirby->url()); + + // 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()); + } + + // 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(); + } + + // 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(); + } + ]; + } +} diff --git a/kirby/src/Cms/LanguageRules.php b/kirby/src/Cms/LanguageRules.php new file mode 100755 index 0000000..9db3ece --- /dev/null +++ b/kirby/src/Cms/LanguageRules.php @@ -0,0 +1,53 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class LanguageRules +{ + public static function create(Language $language): bool + { + if (Str::length($language->code()) < 2) { + throw new InvalidArgumentException([ + 'key' => 'language.code', + 'data' => [ + 'code' => $language->code(), + 'name' => $language->name() + ] + ]); + } + + if (Str::length($language->name()) < 1) { + throw new InvalidArgumentException([ + 'key' => 'language.name', + 'data' => [ + 'code' => $language->code(), + 'name' => $language->name() + ] + ]); + } + + if ($language->exists() === true) { + throw new DuplicateException([ + 'key' => 'language.duplicate', + 'data' => [ + 'code' => $language->code() + ] + ]); + } + + return true; + } +} diff --git a/kirby/src/Cms/Languages.php b/kirby/src/Cms/Languages.php new file mode 100755 index 0000000..e2167da --- /dev/null +++ b/kirby/src/Cms/Languages.php @@ -0,0 +1,109 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Languages extends Collection +{ + /** + * Creates a new collection with the given language objects + * + * @param array $objects + * @param object $parent + */ + public function __construct($objects = [], $parent = null) + { + $defaults = array_filter($objects, function ($language) { + return $language->isDefault() === true; + }); + + if (count($defaults) > 1) { + throw new DuplicateException('You cannot have multiple default languages. Please check your language config files.'); + } + + parent::__construct($objects, $parent); + } + + /** + * 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); + } + + /** + * Returns the default language + * + * @return \Kirby\Cms\Language|null + */ + public function default() + { + if ($language = $this->findBy('isDefault', true)) { + return $language; + } else { + return $this->first(); + } + } + + /** + * @deprecated 3.0.0 Use `Languages::default()` instead + * @return \Kirby\Cms\Language|null + */ + public function findDefault() + { + deprecated('$languages->findDefault() is deprecated, use $languages->default() instead. $languages->findDefault() will be removed in Kirby 3.5.0.'); + + return $this->default(); + } + + /** + * Convert all defined languages to a collection + * + * @internal + * @return self + */ + public static function load() + { + $languages = []; + $files = glob(App::instance()->root('languages') . '/*.php'); + + foreach ($files as $file) { + $props = include $file; + + if (is_array($props) === true) { + // inject the language code from the filename if it does not exist + $props['code'] = $props['code'] ?? F::name($file); + + $languages[] = new Language($props); + } + } + + return new static($languages); + } +} diff --git a/kirby/src/Cms/Media.php b/kirby/src/Cms/Media.php new file mode 100755 index 0000000..804ac3c --- /dev/null +++ b/kirby/src/Cms/Media.php @@ -0,0 +1,155 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Media +{ + /** + * Tries to find a file by model and filename + * and to copy it to the media folder. + * + * @param \Kirby\Cms\Model $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); + + // try to find a file by model and filename + // this should work for all original files + if ($file = $model->file($filename)) { + + // the media hash is outdated. redirect to the correct url + if ($file->mediaHash() !== $hash) { + return Response::redirect($file->mediaUrl(), 307); + } + + // 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); + } + + /** + * Copy the file to the final media folder location + * + * @param string $src + * @param string $dest + * @return bool + */ + public static function publish(string $src, string $dest): bool + { + $filename = basename($src); + $version = dirname($dest); + $directory = dirname($version); + + // unpublish all files except stuff in the version folder + Media::unpublish($directory, $filename, $version); + + // 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 $model + * @param string $hash + * @param string $filename + * @return \Kirby\Cms\Response|false + */ + public static function thumb($model, string $hash, string $filename) + { + $kirby = App::instance(); + + if (is_string($model) === true) { + // assets + $root = $kirby->root('media') . '/assets/' . $model . '/' . $hash; + } else { + // model files + $root = $model->mediaRoot() . '/' . $hash; + } + + try { + $thumb = $root . '/' . $filename; + $job = $root . '/.jobs/' . $filename . '.json'; + $options = Data::read($job); + + 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(); + } + + 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 filename + * within the parent directory + * + * @param string $directory + * @param string $filename + * @param string $ignore + * @return bool + */ + public static function unpublish(string $directory, string $filename, string $ignore = null): bool + { + if (is_dir($directory) === false) { + return true; + } + + $versions = glob($directory . '/' . crc32($filename) . '*', GLOB_ONLYDIR); + + // delete all versions of the file + foreach ($versions as $version) { + if ($version === $ignore) { + continue; + } + + Dir::remove($version); + } + + return true; + } +} diff --git a/kirby/src/Cms/Model.php b/kirby/src/Cms/Model.php new file mode 100755 index 0000000..5788640 --- /dev/null +++ b/kirby/src/Cms/Model.php @@ -0,0 +1,109 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +abstract class Model +{ + use Properties; + + /** + * The parent Kirby instance + * + * @var \Kirby\Cms\App + */ + public static $kirby; + + /** + * 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(); + } + + /** + * 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 = static::$kirby ?? App::instance(); + } + + /** + * Returns the parent Site instance + * + * @return \Kirby\Cms\Site + */ + public function site() + { + return $this->site = $this->site ?? $this->kirby()->site(); + } + + /** + * Setter for the parent Kirby object + * + * @param \Kirby\Cms\App|null $kirby + * @return self + */ + 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 self + */ + 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(); + } +} diff --git a/kirby/src/Cms/ModelPermissions.php b/kirby/src/Cms/ModelPermissions.php new file mode 100755 index 0000000..214c5a7 --- /dev/null +++ b/kirby/src/Cms/ModelPermissions.php @@ -0,0 +1,95 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +abstract class ModelPermissions +{ + protected $category; + protected $model; + protected $options; + protected $permissions; + protected $user; + + public function __call(string $method, array $arguments = []): bool + { + return $this->can($method); + } + + 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(); + } + + public function can(string $action): bool + { + $role = $this->user->role()->id(); + + 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; + } + + // evaluate the blueprint options block + if (isset($this->options[$action]) === true) { + $options = $this->options[$action]; + + if ($options === false) { + return false; + } + + if ($options === true) { + return true; + } + + if (is_array($options) === true && A::isAssociative($options) === true) { + return $options[$role] ?? $options['*'] ?? false; + } + } + + return $this->permissions->for($this->category, $action); + } + + public function cannot(string $action): bool + { + return $this->can($action) === false; + } + + public function toArray(): array + { + $array = []; + + foreach ($this->options as $key => $value) { + $array[$key] = $this->can($key); + } + + return $array; + } +} diff --git a/kirby/src/Cms/ModelWithContent.php b/kirby/src/Cms/ModelWithContent.php new file mode 100755 index 0000000..3bd31ea --- /dev/null +++ b/kirby/src/Cms/ModelWithContent.php @@ -0,0 +1,721 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +abstract class ModelWithContent extends Model +{ + /** + * 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. + */ + const CLASS_ALIAS = null; + + /** + * 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(); + + /** + * 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 $languageCode + * @return \Kirby\Cms\Content + */ + 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 + */ + 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 $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 self + */ + 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) { + if (method_exists($section, 'errors') === true || isset($section->errors)) { + $errors = array_merge($errors, $section->errors()); + } + } + + return $errors; + } + + /** + * Increment a given field value + * + * @param string $field + * @param int $by + * @param int $max + * @return self + */ + 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 icon definition + * + * @internal + * @param array $params + * @return array + */ + public function panelIcon(array $params = null): array + { + $defaults = [ + 'type' => 'page', + 'ratio' => null, + 'back' => 'pattern', + 'color' => '#c5c9c6', + ]; + + return array_merge($defaults, $params ?? []); + } + + /** + * @internal + * @param string|array|false $settings + * @return array|null + */ + public function panelImage($settings = null): ?array + { + $defaults = [ + 'ratio' => '3/2', + 'back' => 'pattern', + 'cover' => false + ]; + + // switch the image off + if ($settings === false) { + return null; + } + + if (is_string($settings) === true) { + $settings = [ + 'query' => $settings + ]; + } + + if ($image = $this->panelImageSource($settings['query'] ?? null)) { + + // main url + $settings['url'] = $image->url(); + + // for cards + $settings['cards'] = [ + 'url' => 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw', + 'srcset' => $image->srcset([ + 352, + 864, + 1408, + ]) + ]; + + // for lists + $settings['list'] = [ + 'url' => 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw', + 'srcset' => $image->srcset([ + '1x' => [ + 'width' => 38, + 'height' => 38, + 'crop' => 'center' + ], + '2x' => [ + 'width' => 76, + 'height' => 76, + 'crop' => 'center' + ], + ]) + ]; + + unset($settings['query']); + } + + return array_merge($defaults, (array)$settings); + } + + /** + * Returns the image file object based on provided query + * + * @internal + * @param string|null $query + * @return \Kirby\Cms\File|\Kirby\Cms\Asset|null + */ + protected function panelImageSource(string $query = null) + { + $image = $this->query($query ?? null); + + // validate the query result + if (is_a($image, 'Kirby\Cms\File') === false && is_a($image, 'Kirby\Cms\Asset') === false) { + $image = null; + } + + // fallback for files + if ($image === null && is_a($this, 'Kirby\Cms\File') === true && $this->isViewable() === true) { + $image = $this; + } + + return $image; + } + + /** + * Returns an array of all actions + * that can be performed in the Panel + * This also checks for the lock status + * @since 3.3.0 + * + * @param array $unlock An array of options that will be force-unlocked + * @return array + */ + public function panelOptions(array $unlock = []): array + { + $options = $this->permissions()->toArray(); + + if ($this->isLocked()) { + foreach ($options as $key => $value) { + if (in_array($key, $unlock)) { + continue; + } + + $options[$key] = false; + } + } + + return $options; + } + + /** + * 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; + } + + $result = Str::query($query, [ + 'kirby' => $this->kirby(), + 'site' => is_a($this, 'Kirby\Cms\Site') ? $this : $this->site(), + static::CLASS_ALIAS => $this + ]); + + 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 string $languageCode + * @param array $data + * @param bool $overwrite + * @return self + */ + 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 self + */ + 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 self + */ + 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 self + */ + 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 $translations + * @return self + */ + 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 + * + * @param string|null $template + * @return string + */ + public function toString(string $template = null): string + { + if ($template === null) { + return $this->id(); + } + + $result = Str::template($template, [ + 'kirby' => $this->kirby(), + 'site' => is_a($this, 'Kirby\Cms\Site') ? $this : $this->site(), + static::CLASS_ALIAS => $this + ]); + + 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 $input + * @param string $languageCode + * @param bool $validate + * @return self + */ + 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() + ]); + } + } + + return $this->commit('update', [$this, $form->data(), $form->strings(), $languageCode], 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 $languageCode + * @return bool + */ + public function writeContent(array $data, string $languageCode = null): bool + { + return Data::write( + $this->contentFile($languageCode), + $this->contentFileData($data, $languageCode) + ); + } +} diff --git a/kirby/src/Cms/Nest.php b/kirby/src/Cms/Nest.php new file mode 100755 index 0000000..21c3eaf --- /dev/null +++ b/kirby/src/Cms/Nest.php @@ -0,0 +1,43 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Nest +{ + public static function create($data, $parent = null) + { + if (is_scalar($data) === true) { + return new Field($parent, $data, $data); + } + + $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); + } + } + + 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 new file mode 100755 index 0000000..2251dcb --- /dev/null +++ b/kirby/src/Cms/NestCollection.php @@ -0,0 +1,33 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +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 $map + * @return array + */ + public function toArray(Closure $map = null): array + { + return parent::toArray($map ?? function ($object) { + return $object->toArray(); + }); + } +} diff --git a/kirby/src/Cms/NestObject.php b/kirby/src/Cms/NestObject.php new file mode 100755 index 0000000..f225376 --- /dev/null +++ b/kirby/src/Cms/NestObject.php @@ -0,0 +1,43 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class NestObject extends Obj +{ + /** + * 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; + } + + if (is_object($value) === true && method_exists($value, 'toArray')) { + $result[$key] = $value->toArray(); + continue; + } + + $result[$key] = $value; + } + + return $result; + } +} diff --git a/kirby/src/Cms/Page.php b/kirby/src/Cms/Page.php new file mode 100755 index 0000000..77aa853 --- /dev/null +++ b/kirby/src/Cms/Page.php @@ -0,0 +1,1543 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Page extends ModelWithContent +{ + const CLASS_ALIAS = 'page'; + + use PageActions; + use PageSiblings; + use HasChildren; + use HasFiles; + use HasMethods; + use HasSiblings; + + /** + * 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 string + */ + 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, $arguments); + } + + /** + * 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->panelId(); + } else { + return $this->kirby()->url('api') . '/pages/' . $this->panelId(); + } + } + + /** + * 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 $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 $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 $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 + */ + public function controller($data = [], $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. + return array_merge($kirby->controller($this->template()->name(), $data, $contentType), $data); + } + + /** + * Returns a number indicating how deep the page + * is nested within the content folder + * + * @return int + */ + public function depth(): int + { + return $this->depth = $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; + } + } + + /** + * 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 $type (null|auto|kirbytext|markdown) + * @return string + */ + public function dragText(string $type = null): string + { + $type = $type ?? 'auto'; + + if ($type === 'auto') { + $type = option('panel.kirbytext', true) ? 'kirbytext' : 'markdown'; + } + + switch ($type) { + case 'markdown': + return '[' . $this->title() . '](' . $this->url() . ')'; + default: + return '(link: ' . $this->id() . ' text: ' . $this->title() . ')'; + } + } + + /** + * 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 self + */ + public static function factory($props) + { + if (empty($props['model']) === false) { + return static::model($props['model'], $props); + } + + return new static($props); + } + + /** + * 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(); + } + + /** + * 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 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; + } + + /** + * @deprecated 3.0.0 Use `Page::isUnlisted()` instead + * @return bool + */ + public function isInvisible(): bool + { + deprecated('$page->isInvisible() is deprecated, use $page->isUnlisted() instead. $page->isInvisible() will be removed in Kirby 3.5.0.'); + + return $this->isUnlisted(); + } + + /** + * 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 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; + } + + /** + * @deprecated 3.0.0 Use `Page::isListed()` instead + * @return bool + */ + public function isVisible(): bool + { + deprecated('$page->isVisible() is deprecated, use $page->isListed() instead. $page->isVisible() will be removed in Kirby 3.5.0.'); + + return $this->isListed(); + } + + /** + * Checks if the page access is verified. + * This is only used for drafts so far. + * + * @internal + * @param string $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 self + */ + 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 $format + * @param string|null $handler + * @return int|string + */ + public function modified(string $format = null, string $handler = null) + { + return F::modified($this->contentFile(), $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 icon definition + * according to the blueprint settings + * + * @internal + * @param array $params + * @return array + */ + public function panelIcon(array $params = null): array + { + if ($icon = $this->blueprint()->icon()) { + $params['type'] = $icon; + + // check for emojis + if (strlen($icon) !== Str::length($icon)) { + $params['emoji'] = true; + } + } + + return parent::panelIcon($params); + } + + /** + * Returns the escaped Id, which is + * used in the panel to make routing work properly + * + * @internal + * @return string + */ + public function panelId(): string + { + return str_replace('/', '+', $this->id()); + } + + /** + * Returns the image file object based on provided query + * + * @internal + * @param string|null $query + * @return \Kirby\Cms\File|\Kirby\Cms\Asset|null + */ + protected function panelImageSource(string $query = null) + { + if ($query === null) { + $query = 'page.image'; + } + + return parent::panelImageSource($query); + } + + /** + * Returns the full path without leading slash + * + * @internal + * @return string + */ + public function panelPath(): string + { + return 'pages/' . $this->panelId(); + } + + /** + * Prepares the response data for page pickers + * and page fields + * + * @param array|null $params + * @return array + */ + public function panelPickerData(array $params = []): array + { + $image = $this->panelImage($params['image'] ?? []); + $icon = $this->panelIcon($image); + + return [ + 'dragText' => $this->dragText(), + 'hasChildren' => $this->hasChildren(), + 'icon' => $icon, + 'id' => $this->id(), + 'image' => $image, + 'info' => $this->toString($params['info'] ?? false), + 'link' => $this->panelUrl(true), + 'text' => $this->toString($params['text'] ?? '{{ page.title }}'), + 'url' => $this->url(), + ]; + } + + /** + * Returns the url to the editing view + * in the panel + * + * @internal + * @param bool $relative + * @return string + */ + public function panelUrl(bool $relative = false): string + { + if ($relative === true) { + return '/' . $this->panelPath(); + } else { + return $this->kirby()->url('panel') . '/' . $this->panelPath(); + } + } + + /** + * 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 + * @param int $code + * @return string + */ + 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) { + $kirby->data = $this->controller($data, $contentType); + + if ($contentType === 'html') { + $template = $this->template(); + } else { + $template = $this->representation($contentType); + } + + if ($template->exists() === false) { + throw new NotFoundException([ + 'key' => 'template.default.notFound' + ]); + } + + // 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) { + $cache->set($cacheId, [ + 'html' => $html, + 'response' => $response + ]); + } + } + + return $html; + } + + /** + * @internal + * @param mixed $type + * @return \Kirby\Cms\Template + */ + public function representation($type) + { + $kirby = $this->kirby(); + $template = $this->intendedTemplate(); + $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->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 $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 self + */ + 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 $dirname + * @return self + */ + protected function setDirname(string $dirname = null) + { + $this->dirname = $dirname; + return $this; + } + + /** + * Sets the draft flag + * + * @param bool $isDraft + * @return self + */ + protected function setIsDraft(bool $isDraft = null) + { + $this->isDraft = $isDraft ?? false; + return $this; + } + + /** + * Sets the sorting number + * + * @param int $num + * @return self + */ + 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 self + */ + protected function setParent(Page $parent = null) + { + $this->parent = $parent; + return $this; + } + + /** + * Sets the absolute path to the page + * + * @param string|null $root + * @return self + */ + protected function setRoot(string $root = null) + { + $this->root = $root; + return $this; + } + + /** + * Sets the required Page slug + * + * @param string $slug + * @return self + */ + protected function setSlug(string $slug) + { + $this->slug = $slug; + return $this; + } + + /** + * Sets the intended template + * + * @param string $template + * @return self + */ + protected function setTemplate(string $template = null) + { + if ($template !== null) { + $this->intendedTemplate = $this->kirby()->template($template); + } + + return $this; + } + + /** + * Sets the Url + * + * @param string $url + * @return self + */ + 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(); + } + + if ($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 sha1($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 $language + * @param array $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); + } +} diff --git a/kirby/src/Cms/PageActions.php b/kirby/src/Cms/PageActions.php new file mode 100755 index 0000000..6c4a2d8 --- /dev/null +++ b/kirby/src/Cms/PageActions.php @@ -0,0 +1,788 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait PageActions +{ + /** + * Changes the sorting number + * The sorting number must already be correct + * when the method is called + * + * @param int $num + * @return self + */ + 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', [$this, $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 $languageCode + * @return self + */ + 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; + } + + return $this->commit('changeSlug', [$this, $slug, $languageCode = null], 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 $languageCode + * @return self + */ + 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'); + } + + return $this->commit('changeSlug', [$this, $slug, $languageCode], 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 + * + * @param string $status "draft", "listed" or "unlisted" + * @param int $position Optional sorting number + * @return self + */ + 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 Exception('Invalid status: ' . $status); + } + } + + protected function changeStatusToDraft() + { + $page = $this->commit('changeStatus', [$this, 'draft', null], function ($page) { + return $page->unpublish(); + }); + + return $page; + } + + /** + * @param int $position + * @return self + */ + 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; + } + + $page = $this->commit('changeStatus', [$this, 'listed', $num], function ($page, $status, $position) { + return $page->publish()->changeNum($position); + }); + + if ($this->blueprint()->num() === 'default') { + $page->resortSiblingsAfterListing($num); + } + + return $page; + } + + /** + * @return self + */ + protected function changeStatusToUnlisted() + { + if ($this->status() === 'unlisted') { + return $this; + } + + $page = $this->commit('changeStatus', [$this, 'unlisted', null], function ($page) { + return $page->publish()->changeNum(null); + }); + + $this->resortSiblingsAfterUnlisting(); + + return $page; + } + + /** + * Changes the page template + * + * @param string $template + * @return self + */ + public function changeTemplate(string $template) + { + if ($template === $this->template()->name()) { + return $this; + } + + return $this->commit('changeTemplate', [$this, $template], function ($oldPage, $template) { + if ($this->kirby()->multilang() === true) { + $newPage = $this->clone([ + 'template' => $template + ]); + + foreach ($this->kirby()->languages()->codes() as $code) { + $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 + return $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'); + } + + return $newPage->save(); + } + }); + } + + /** + * Change the page title + * + * @param string $title + * @param string|null $languageCode + * @return self + */ + public function changeTitle(string $title, string $languageCode = null) + { + return $this->commit('changeTitle', [$this, $title, $languageCode], function ($page, $title, $languageCode) { + return $page->save(['title' => $title], $languageCode); + }); + } + + /** + * 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(); + + $this->rules()->$action(...$arguments); + $this->kirby()->trigger('page.' . $action . ':before', ...$arguments); + $result = $callback(...$arguments); + $this->kirby()->trigger('page.' . $action . ':after', $result, $old); + $this->kirby()->cache('pages')->flush(); + return $result; + } + + /** + * Copies the page to a new parent + * + * @param array $options + * @return \Kirby\Cms\Page + */ + 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 = []; + + // 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 = $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 self + */ + 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, $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 self + */ + public function createChild(array $props) + { + $props = array_merge($props, [ + 'url' => null, + 'num' => null, + 'parent' => $this, + 'site' => $this->site(), + ]); + + return static::create($props); + } + + /** + * Create the sorting number for the page + * depending on the blueprint settings + * + * @param int $num + * @return int + */ + public function createNum(int $num = null): int + { + $mode = $this->blueprint()->num(); + + switch ($mode) { + case 'zero': + return 0; + case 'date': + case 'datetime': + $format = $mode === 'date' ? 'Ymd' : 'YmdHi'; + $lang = $this->kirby()->defaultLanguage() ?? null; + $field = $this->content($lang)->get('date'); + $date = $field->isEmpty() ? 'now' : $field; + return date($format, strtotime($date)); + break; + 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(); + $app->setCurrentLanguage(); + + $template = Str::template($mode, [ + 'kirby' => $app, + 'page' => $app->page($this->id()), + 'site' => $app->site(), + ]); + + return (int)$template; + } + } + + /** + * Deletes the page + * + * @param bool $force + * @return bool + */ + public function delete(bool $force = false): bool + { + return $this->commit('delete', [$this, $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 $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() . '-copy'); + + return $this->commit('duplicate', [$this, $slug, $options], function ($page, $slug, $options) { + return $this->copy([ + 'parent' => $this->parent(), + 'slug' => $slug, + 'isDraft' => true, + 'files' => $options['files'] ?? false, + 'children' => $options['children'] ?? false, + ]); + }); + } + + 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 self + */ + 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; + } + + protected function resortSiblingsAfterListing(int $position = null): bool + { + // get all siblings including the current page + $siblings = $this + ->parentModel() + ->children() + ->listed() + ->append($this) + ->filter(function ($page) { + return $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; + } else { + if ($sibling = $siblings->get($id)) { + $sibling->changeNum($key + 1); + } + } + } + + $parent = $this->parentModel(); + $parent->children = $parent->children()->sortBy('num', 'asc'); + + return true; + } + + public function resortSiblingsAfterUnlisting(): bool + { + $index = 0; + $parent = $this->parentModel(); + $siblings = $parent + ->children() + ->listed() + ->not($this) + ->filter(function ($page) { + return $page->blueprint()->num() === 'default'; + }); + + if ($siblings->count() > 0) { + foreach ($siblings as $sibling) { + $index++; + $sibling->changeNum($index); + } + + $parent->children = $siblings->sortBy('num', 'asc'); + } + + return true; + } + + public function sort($position = null) + { + return $this->changeStatus('listed', $position); + } + + /** + * Convert a page from listed or + * unlisted to draft. + * + * @return self + */ + 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 $input + * @param string $language + * @param bool $validate + * @return self + */ + public function update(array $input = null, string $language = null, bool $validate = false) + { + if ($this->isDraft() === true) { + $validate = false; + } + + $page = parent::update($input, $language, $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; + } +} diff --git a/kirby/src/Cms/PageBlueprint.php b/kirby/src/Cms/PageBlueprint.php new file mode 100755 index 0000000..3771e4b --- /dev/null +++ b/kirby/src/Cms/PageBlueprint.php @@ -0,0 +1,209 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +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); + + // 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 the ordering number + $this->props['num'] = $this->normalizeNum($props['num'] ?? 'default'); + + // normalize the available status array + $this->props['status'] = $this->normalizeStatus($props['status'] ?? null); + } + + /** + * 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', + ]; + + if (isset($aliases[$num]) === true) { + return $aliases[$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'), + ] + ]; + + // use the defaults, when the status is not defined + if (empty($status) === true) { + $status = $defaults; + } + + // extend the status definition + $status = $this->extend($status); + + // 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; + } + + 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 + ]; + } + + // 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; + } + + // 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; + } + + // remove the draft status for the home and error pages + if ($this->model->isHomeOrErrorPage() === true) { + unset($status['draft']); + } + + return $status; + } + + /** + * 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 controlls 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); + } + + return $preview; + } + + /** + * 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 new file mode 100755 index 0000000..1f79353 --- /dev/null +++ b/kirby/src/Cms/PagePermissions.php @@ -0,0 +1,62 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class PagePermissions extends ModelPermissions +{ + protected $category = 'pages'; + + protected function canChangeSlug(): bool + { + return $this->model->isHomeOrErrorPage() !== true; + } + + protected function canChangeStatus(): bool + { + return $this->model->isErrorPage() !== true; + } + + protected function canChangeTemplate(): bool + { + if ($this->model->isHomeOrErrorPage() === true) { + return false; + } + + if (count($this->model->blueprints()) <= 1) { + return false; + } + + return true; + } + + protected function canDelete(): bool + { + return $this->model->isHomeOrErrorPage() !== true; + } + + protected function canSort(): bool + { + if ($this->model->isErrorPage() === true) { + return false; + } + + if ($this->model->isListed() !== true) { + return false; + } + + if ($this->model->blueprint()->num() !== 'default') { + return false; + } + + return true; + } +} diff --git a/kirby/src/Cms/PagePicker.php b/kirby/src/Cms/PagePicker.php new file mode 100755 index 0000000..e8529ac --- /dev/null +++ b/kirby/src/Cms/PagePicker.php @@ -0,0 +1,264 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class PagePicker extends Picker +{ + /** + * @var \Kirby\Cms\Pages + */ + protected $items; + + /** + * @var \Kirby\Cms\Pages + */ + protected $itemsForQuery; + + /** + * @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, + ]); + } + + /** + * 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(); + } + + 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(); + } + + 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 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() + ]; + } + + /** + * 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(); + + // 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(); + } + + // filter protected pages + $items = $items->filterBy('isReadable', true); + + // search + $items = $this->search($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 query string + * + * @return \Kirby\Cms\Pages + */ + public function itemsForQuery() + { + // cache + if ($this->itemsForQuery !== null) { + return $this->itemsForQuery; + } + + $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'); + } + + 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; + } + + 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(); + } + + 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()); + + return $array; + } +} diff --git a/kirby/src/Cms/PageRules.php b/kirby/src/Cms/PageRules.php new file mode 100755 index 0000000..d385237 --- /dev/null +++ b/kirby/src/Cms/PageRules.php @@ -0,0 +1,298 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class PageRules +{ + public static function changeNum(Page $page, int $num = null): bool + { + if ($num !== null && $num < 0) { + throw new InvalidArgumentException(['key' => 'page.num.invalid']); + } + + return true; + } + + 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() + ] + ]); + } + + $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 = $drafts->find($slug)) { + if ($duplicate->is($page) === false) { + throw new DuplicateException([ + 'key' => 'page.draft.duplicate', + 'data' => [ + 'slug' => $slug + ] + ]); + } + } + + return true; + } + + 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']); + } + } + + 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() + ] + ]); + } + + return true; + } + + 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; + } + + if ($page->permissions()->changeStatus() !== true) { + throw new PermissionException([ + 'key' => 'page.changeStatus.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } + + if ($position !== null && $position < 0) { + throw new InvalidArgumentException(['key' => 'page.num.invalid']); + } + + if ($page->isDraft() === true && empty($page->errors()) === false) { + throw new PermissionException([ + 'key' => 'page.changeStatus.incomplete', + 'details' => $page->errors() + ]); + } + + return true; + } + + public static function changeStatusToUnlisted(Page $page) + { + if ($page->permissions()->changeStatus() !== true) { + throw new PermissionException([ + 'key' => 'page.changeStatus.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } + + return true; + } + + 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()] + ]); + } + + return true; + } + + public static function changeTitle(Page $page, string $title): bool + { + if (Str::length($title) === 0) { + throw new InvalidArgumentException([ + 'key' => 'page.changeTitle.empty', + ]); + } + + if ($page->permissions()->changeTitle() !== true) { + throw new PermissionException([ + 'key' => 'page.changeTitle.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } + + return true; + } + + public static function create(Page $page): bool + { + if (Str::length($page->slug()) < 1) { + throw new InvalidArgumentException([ + 'key' => 'page.slug.invalid', + ]); + } + + if ($page->exists() === true) { + throw new DuplicateException([ + 'key' => 'page.draft.duplicate', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } + + if ($page->permissions()->create() !== true) { + throw new PermissionException([ + 'key' => 'page.create.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } + + $siblings = $page->parentModel()->children(); + $drafts = $page->parentModel()->drafts(); + $slug = $page->slug(); + + if ($duplicate = $siblings->find($slug)) { + throw new DuplicateException([ + 'key' => 'page.duplicate', + 'data' => ['slug' => $slug] + ]); + } + + if ($duplicate = $drafts->find($slug)) { + throw new DuplicateException([ + 'key' => 'page.draft.duplicate', + 'data' => ['slug' => $slug] + ]); + } + + return true; + } + + 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']); + } + + return true; + } + + 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() + ] + ]); + } + + return true; + } + + 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; + } +} diff --git a/kirby/src/Cms/PageSiblings.php b/kirby/src/Cms/PageSiblings.php new file mode 100755 index 0000000..0aa6a17 --- /dev/null +++ b/kirby/src/Cms/PageSiblings.php @@ -0,0 +1,212 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait PageSiblings +{ + /** + * @deprecated 3.0.0 Use `Page::hasNextUnlisted()` instead + * @return bool + */ + public function hasNextInvisible(): bool + { + deprecated('$page->hasNextInvisible() is deprecated, use $page->hasNextUnlisted() instead. $page->hasNextInvisible() will be removed in Kirby 3.5.0.'); + + return $this->hasNextUnlisted(); + } + + /** + * Checks if there's a next listed + * page in the siblings collection + * + * @return bool + */ + public function hasNextListed(): bool + { + return $this->nextListed() !== null; + } + + /** + * Checks if there's a next unlisted + * page in the siblings collection + * + * @return bool + */ + public function hasNextUnlisted(): bool + { + return $this->nextUnlisted() !== null; + } + + /** + * @deprecated 3.0.0 Use `Page::hasNextListed()` instead + * @return bool + */ + public function hasNextVisible(): bool + { + deprecated('$page->hasNextVisible() is deprecated, use $page->hasNextListed() instead. $page->hasNextVisible() will be removed in Kirby 3.5.0.'); + + return $this->hasNextListed(); + } + + /** + * @deprecated 3.0.0 Use `Page::hasPrevUnlisted()` instead + * @return bool + */ + public function hasPrevInvisible(): bool + { + deprecated('$page->hasPrevInvisible() is deprecated, use $page->hasPrevUnlisted() instead. $page->hasPrevInvisible() will be removed in Kirby 3.5.0.'); + + return $this->hasPrevUnlisted(); + } + + /** + * Checks if there's a previous listed + * page in the siblings collection + * + * @return bool + */ + public function hasPrevListed(): bool + { + return $this->prevListed() !== null; + } + + /** + * Checks if there's a previous unlisted + * page in the siblings collection + * + * @return bool + */ + public function hasPrevUnlisted(): bool + { + return $this->prevUnlisted() !== null; + } + + /** + * @deprecated 3.0.0 Use `Page::hasPrevListed()` instead + * @return bool + */ + public function hasPrevVisible(): bool + { + deprecated('$page->hasPrevVisible() is deprecated, use $page->hasPrevListed() instead. $page->hasPrevVisible() will be removed in Kirby 3.5.0.'); + + return $this->hasPrevListed(); + } + + /** + * @deprecated 3.0.0 Use `Page::nextUnlisted()` instead + * @return self|null + */ + public function nextInvisible() + { + deprecated('$page->nextInvisible() is deprecated, use $page->nextUnlisted() instead. $page->nextInvisible() will be removed in Kirby 3.5.0.'); + + return $this->nextUnlisted(); + } + + /** + * Returns the next listed page if it exists + * + * @return \Kirby\Cms\Page|null + */ + public function nextListed() + { + return $this->nextAll()->listed()->first(); + } + + /** + * Returns the next unlisted page if it exists + * + * @return \Kirby\Cms\Page|null + */ + public function nextUnlisted() + { + return $this->nextAll()->unlisted()->first(); + } + + /** + * @deprecated 3.0.0 Use `Page::nextListed()` instead + * @return self|null + */ + public function nextVisible() + { + deprecated('$page->nextVisible() is deprecated, use $page->nextListed() instead. $page->nextVisible() will be removed in Kirby 3.5.0.'); + + return $this->nextListed(); + } + + /** + * @deprecated 3.0.0 Use `Page::prevUnlisted()` instead + * @return self|null + */ + public function prevInvisible() + { + deprecated('$page->prevInvisible() is deprecated, use $page->prevUnlisted() instead. $page->prevInvisible() will be removed in Kirby 3.5.0.'); + + return $this->prevUnlisted(); + } + + /** + * Returns the previous listed page + * + * @return \Kirby\Cms\Page|null + */ + public function prevListed() + { + return $this->prevAll()->listed()->last(); + } + + /** + * Returns the previous unlisted page + * + * @return \Kirby\Cms\Page|null + */ + public function prevUnlisted() + { + return $this->prevAll()->unlisted()->first(); + } + + /** + * @deprecated 3.0.0 Use `Page::prevListed()` instead + * @return self|null + */ + public function prevVisible() + { + deprecated('$page->prevVisible() is deprecated, use $page->prevListed() instead. $page->prevVisible() will be removed in Kirby 3.5.0.'); + + return $this->prevListed(); + } + + /** + * 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)->filterBy('intendedTemplate', $this->intendedTemplate()->name()); + } +} diff --git a/kirby/src/Cms/Pages.php b/kirby/src/Cms/Pages.php new file mode 100755 index 0000000..d8fb6fb --- /dev/null +++ b/kirby/src/Cms/Pages.php @@ -0,0 +1,523 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Pages extends Collection +{ + /** + * Cache for the index + * + * @var \Kirby\Cms\Pages|null + */ + protected $index = null; + + /** + * All registered pages methods + * + * @var array + */ + public static $methods = []; + + /** + * Adds a single page or + * an entire second collection to the + * current collection + * + * @param mixed $object + * @return self + */ + public function add($object) + { + // add a page collection + if (is_a($object, static::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 + } elseif (in_array($object, [null, false, true], true) !== true) { + throw new InvalidArgumentException('You must pass a Page object to the Pages collection'); + } + + return $this; + } + + /** + * Returns all audio files of all children + * + * @return \Kirby\Cms\Files + */ + public function audio() + { + return $this->files()->filterBy('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()->filterBy('type', 'code'); + } + + /** + * Returns all documents of all children + * + * @return \Kirby\Cms\Files + */ + public function documents() + { + return $this->files()->filterBy('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 $model + * @param bool $draft + * @return self + */ + public static function factory(array $pages, Model $model = null, bool $draft = false) + { + $model = $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) + { + // 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; + } + + $multiLang = App::instance()->multilang(); + + if ($multiLang === true && $page = $this->findBy('slug', $id)) { + return $page; + } + + $start = is_a($this->parent, 'Kirby\Cms\Page') === true ? $this->parent->id() : ''; + $page = $this->findByIdRecursive($id, $start, $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); + $collection = $this; + $item = null; + $query = $startAt; + + foreach ($path as $key) { + $query = ltrim($query . '/' . $key, '/'); + $item = $collection->get($query) ?? null; + + if ($item === null && $multiLang === true) { + $item = $collection->findBy('slug', $key); + } + + if ($item === null) { + return null; + } + + $collection = $item->children(); + } + + 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()->filterBy('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) + { + if (is_a($this->index, 'Kirby\Cms\Pages') === true) { + return $this->index; + } + + $this->index = new Pages([], $this->parent); + + foreach ($this->data as $pageKey => $page) { + $this->index->data[$pageKey] = $page; + + foreach ($page->index($drafts) as $childKey => $child) { + $this->index->data[$childKey] = $child; + } + } + + return $this->index; + } + + /** + * @deprecated 3.0.0 Use `Pages::unlisted()` instead + * + * @return self + */ + public function invisible() + { + deprecated('$pages->invisible() is deprecated, use $pages->unlisted() instead. $pages->invisible() will be removed in Kirby 3.5.0.'); + + return $this->unlisted(); + } + + /** + * Returns all listed pages in the collection + * + * @return \Kirby\Cms\Pages + */ + public function listed() + { + return $this->filterBy('isListed', '==', true); + } + + /** + * Returns all unlisted pages in the collection + * + * @return \Kirby\Cms\Pages + */ + public function unlisted() + { + return $this->filterBy('isUnlisted', '==', true); + } + + /** + * Include all given items in the collection + * + * @param mixed ...$args + * @return self + */ + 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], static::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->filterBy('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()->filterBy('type', 'video'); + } + + /** + * @deprecated 3.0.0 Use `Pages::listed()` instead + * + * @return \Kirby\Cms\Pages + */ + public function visible() + { + deprecated('$pages->visible() is deprecated, use $pages->listed() instead. $pages->visible() will be removed in Kirby 3.5.0.'); + + return $this->listed(); + } +} diff --git a/kirby/src/Cms/Pagination.php b/kirby/src/Cms/Pagination.php new file mode 100755 index 0000000..4f6171b --- /dev/null +++ b/kirby/src/Cms/Pagination.php @@ -0,0 +1,177 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Pagination extends BasePagination +{ + /** + * Pagination method (param or query) + * + * @var string + */ + protected $method; + + /** + * The base URL + * + * @var string + */ + protected $url; + + /** + * 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(); + + $params['limit'] = $params['limit'] ?? $config['limit'] ?? 20; + $params['method'] = $params['method'] ?? $config['method'] ?? 'param'; + $params['variable'] = $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 ($params['method'] === 'query') { + $params['page'] = $params['page'] ?? $params['url']->query()->get($params['variable']); + } else { + $params['page'] = $params['page'] ?? $params['url']->params()->get($params['variable']); + } + + parent::__construct($params); + + $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 last page + * + * @return string + */ + 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); + } + + 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()); + } + + $url = clone $this->url; + $variable = $this->variable; + + if ($this->hasPage($page) === false) { + return null; + } + + $pageValue = $page === 1 ? null : $page; + + if ($this->method === 'query') { + $url->query->$variable = $pageValue; + } else { + $url->params->$variable = $pageValue; + } + + 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); + } + + return null; + } +} diff --git a/kirby/src/Cms/Panel.php b/kirby/src/Cms/Panel.php new file mode 100755 index 0000000..cc3edc2 --- /dev/null +++ b/kirby/src/Cms/Panel.php @@ -0,0 +1,126 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Panel +{ + public static function customCss(App $kirby) + { + if ($css = $kirby->option('panel.css')) { + $asset = asset($css); + + if ($asset->exists() === true) { + return $asset->url() . '?' . $asset->modified(); + } + } + + return false; + } + + public static function icons(App $kirby): string + { + return F::read($kirby->root('kirby') . '/panel/dist/img/icons.svg'); + } + + /** + * Links all dist files in the media folder + * and returns the link to the requested asset + * + * @param \Kirby\Cms\App $kirby + * @return bool + */ + public static function link(App $kirby): bool + { + $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; + } + + // delete the panel folder and all previous versions + Dir::remove($mediaRoot); + + // recreate the panel folder + Dir::make($mediaRoot, true); + + // create a symlink to the dist folder + if (Dir::copy($panelRoot, $versionRoot) !== true) { + throw new Exception('Panel assets could not be linked'); + } + + return true; + } + + /** + * Renders the main panel view + * + * @param \Kirby\Cms\App $kirby + * @return \Kirby\Cms\Response + */ + public static function render(App $kirby) + { + try { + if (static::link($kirby) === true) { + usleep(1); + go($kirby->url('index') . '/' . $kirby->path()); + } + } catch (Throwable $e) { + die('The panel assets cannot be installed properly. Please check permissions of your media folder.'); + } + + // get the uri object for the panel url + $uri = new Uri($url = $kirby->url('panel')); + + // fetch all plugins + $plugins = new PanelPlugins(); + + $view = new View($kirby->root('kirby') . '/views/panel.php', [ + 'kirby' => $kirby, + 'config' => $kirby->option('panel'), + 'assetUrl' => $kirby->url('media') . '/panel/' . $kirby->versionHash(), + 'customCss' => static::customCss($kirby), + 'icons' => static::icons($kirby), + 'pluginCss' => $plugins->url('css'), + 'pluginJs' => $plugins->url('js'), + 'panelUrl' => $uri->path()->toString(true) . '/', + 'nonce' => $kirby->nonce(), + 'options' => [ + 'url' => $url, + 'site' => $kirby->url('index'), + 'api' => $kirby->url('api'), + 'csrf' => $kirby->option('api.csrf') ?? csrf(), + 'translation' => 'en', + 'debug' => $kirby->option('debug', false), + 'search' => [ + 'limit' => $kirby->option('panel.search.limit') ?? 10 + ] + ] + ]); + + return new Response($view->render()); + } +} diff --git a/kirby/src/Cms/PanelPlugins.php b/kirby/src/Cms/PanelPlugins.php new file mode 100755 index 0000000..e05caea --- /dev/null +++ b/kirby/src/Cms/PanelPlugins.php @@ -0,0 +1,108 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class PanelPlugins +{ + /** + * 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; + } + + $this->files = []; + + foreach (App::instance()->plugins() as $plugin) { + $this->files[] = $plugin->root() . '/index.css'; + $this->files[] = $plugin->root() . '/index.js'; + } + + return $this->files; + } + + /** + * 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); + } + + return max($modified); + } + + /** + * 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); + + // make sure that each plugin is ended correctly + if (Str::endsWith($content, ';') === false) { + $content .= ';'; + } + } + + $dist[] = $content; + } + } + } + + 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/Cms/Permissions.php b/kirby/src/Cms/Permissions.php new file mode 100755 index 0000000..48891ec --- /dev/null +++ b/kirby/src/Cms/Permissions.php @@ -0,0 +1,169 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Permissions +{ + protected $actions = [ + 'access' => [ + 'panel' => true, + 'settings' => true, + 'site' => true, + 'users' => true, + ], + 'files' => [ + 'changeName' => true, + 'create' => true, + 'delete' => 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 + ] + ]; + + public function __construct($settings = []) + { + if (is_array($settings) === true) { + return $this->setCategories($settings); + } + + if (is_bool($settings) === true) { + return $this->setAll($settings); + } + } + + public function for(string $category = null, string $action = null): bool + { + if ($action === null) { + if ($this->hasCategory($category) === false) { + return false; + } + + return $this->actions[$category]; + } + + if ($this->hasAction($category, $action) === false) { + return false; + } + + return $this->actions[$category][$action]; + } + + protected function hasAction(string $category, string $action): bool + { + return $this->hasCategory($category) === true && array_key_exists($action, $this->actions[$category]) === true; + } + + protected function hasCategory(string $category): bool + { + return array_key_exists($category, $this->actions) === true; + } + + protected function setAction(string $category, string $action, $setting) + { + // wildcard to overwrite the entire category + if ($action === '*') { + return $this->setCategory($category, $setting); + } + + $this->actions[$category][$action] = $setting; + + return $this; + } + + protected function setAll(bool $setting) + { + foreach ($this->actions as $categoryName => $actions) { + $this->setCategory($categoryName, $setting); + } + + 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); + } + } + } + + return $this; + } + + 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; + } + + return $this; + } + + public function toArray(): array + { + return $this->actions; + } +} diff --git a/kirby/src/Cms/Picker.php b/kirby/src/Cms/Picker.php new file mode 100755 index 0000000..c034a63 --- /dev/null +++ b/kirby/src/Cms/Picker.php @@ -0,0 +1,176 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +abstract class Picker +{ + /** + * @var \Kirby\Cms\App + */ + protected $kirby; + + /** + * @var array + */ + protected $options; + + /** + * @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(); + } + + /** + * 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, + // 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 + ]; + } + + /** + * 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 []; + } + + $result = []; + + foreach ($items as $index => $item) { + if (empty($this->options['map']) === false) { + $result[] = $this->options['map']($item); + } else { + $result[] = $item->panelPickerData([ + 'image' => $this->options['image'], + 'info' => $this->options['info'], + 'model' => $this->options['model'], + 'text' => $this->options['text'], + ]); + } + } + + 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($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() + ]; + } + + /** + * Search through the collection of items + * if not deactivate in the options + * + * @param \Kirby\Cms\Collection $items + * @return \Kirby\Cms\Collection + */ + public function search($items) + { + if (empty($this->options['search']) === false) { + return $items->search($this->options['search']); + } + + 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(); + + return [ + 'data' => $this->itemsToArray($items), + 'pagination' => $this->paginationToArray($items->pagination()), + ]; + } +} diff --git a/kirby/src/Cms/Plugin.php b/kirby/src/Cms/Plugin.php new file mode 100755 index 0000000..e0dedc5 --- /dev/null +++ b/kirby/src/Cms/Plugin.php @@ -0,0 +1,115 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Plugin extends Model +{ + protected $extends; + protected $info; + protected $name; + protected $root; + + public function __call(string $key, array $arguments = null) + { + return $this->info()[$key] ?? null; + } + + public function __construct(string $name, array $extends = []) + { + $this->setName($name); + $this->extends = $extends; + $this->root = $extends['root'] ?? dirname(debug_backtrace()[0]['file']); + + unset($this->extends['root']); + } + + public function extends(): array + { + return $this->extends; + } + + 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 = []; + } + + return $this->info = $info; + } + + public function manifest(): string + { + return $this->root() . '/composer.json'; + } + + public function mediaRoot(): string + { + return App::instance()->root('media') . '/plugins/' . $this->name(); + } + + public function mediaUrl(): string + { + return App::instance()->url('media') . '/plugins/' . $this->name(); + } + + public function name(): string + { + return $this->name; + } + + public function option(string $key) + { + return $this->kirby()->option($this->prefix() . '.' . $key); + } + + public function prefix(): string + { + return str_replace('/', '.', $this->name()); + } + + public function root(): string + { + return $this->root; + } + + /** + * @param string $name + * @return self + */ + protected function setName(string $name) + { + if (preg_match('!^[a-z0-9-]+\/[a-z0-9-]+$!i', $name) == false) { + throw new InvalidArgumentException('The plugin name must follow the format "a-z0-9-/a-z0-9-"'); + } + + $this->name = $name; + return $this; + } + + public function toArray(): array + { + return $this->propertiesToArray(); + } +} diff --git a/kirby/src/Cms/PluginAssets.php b/kirby/src/Cms/PluginAssets.php new file mode 100755 index 0000000..20f53c8 --- /dev/null +++ b/kirby/src/Cms/PluginAssets.php @@ -0,0 +1,84 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +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); + + foreach ($assets as $asset) { + $original = $root . '/' . $asset; + + if (file_exists($original) === false) { + $assetRoot = $media . '/' . $asset; + + 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; + + if (F::exists($source, $plugin->root()) === true) { + // do some spring cleaning for older files + static::clean($pluginName); + + $target = $plugin->mediaRoot() . '/' . $filename; + $url = $plugin->mediaUrl() . '/' . $filename; + + // create the plugin directory first + Dir::make($plugin->mediaRoot(), true); + + if (F::link($source, $target, 'symlink') === true) { + return Response::redirect($url); + } + + return Response::file($source); + } + } + + return null; + } +} diff --git a/kirby/src/Cms/R.php b/kirby/src/Cms/R.php new file mode 100755 index 0000000..9c9c354 --- /dev/null +++ b/kirby/src/Cms/R.php @@ -0,0 +1,25 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class R extends Facade +{ + /** + * @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 new file mode 100755 index 0000000..0ed5169 --- /dev/null +++ b/kirby/src/Cms/Responder.php @@ -0,0 +1,222 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Responder +{ + /** + * HTTP status code + * + * @var int + */ + protected $code = null; + + /** + * Response body + * + * @var string + */ + protected $body = null; + + /** + * HTTP headers + * + * @var array + */ + protected $headers = []; + + /** + * Content type + * + * @var string + */ + protected $type = null; + + /** + * Creates and sends the response + * + * @return string + */ + public function __toString(): string + { + return (string)$this->send(); + } + + /** + * Setter and getter for the response body + * + * @param string $body + * @return string|self + */ + public function body(string $body = null) + { + if ($body === null) { + return $this->body; + } + + $this->body = $body; + return $this; + } + + /** + * Setter and getter for the status code + * + * @param int $code + * @return int|self + */ + public function code(int $code = null) + { + if ($code === null) { + return $this->code; + } + + $this->code = $code; + return $this; + } + + /** + * Construct response from an array + * + * @param array $response + */ + public function fromArray(array $response): void + { + $this->body($response['body'] ?? null); + $this->code($response['code'] ?? null); + $this->headers($response['headers'] ?? null); + $this->type($response['type'] ?? null); + } + + /** + * Setter and getter for a single header + * + * @param string $key + * @param string|false|null $value + * @return string|self + */ + public function header(string $key, $value = null) + { + if ($value === null) { + return $this->headers[$key] ?? null; + } + + if ($value === false) { + unset($this->headers[$key]); + return $this; + } + + $this->headers[$key] = $value; + return $this; + } + + /** + * Setter and getter for all headers + * + * @param array $headers + * @return array|self + */ + public function headers(array $headers = null) + { + if ($headers === null) { + return $this->headers; + } + + $this->headers = $headers; + return $this; + } + + /** + * Shortcut to configure a json response + * + * @param array $json + * @return string|self + */ + 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 self + */ + 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 + { + return [ + 'body' => $this->body, + 'code' => $this->code, + 'headers' => $this->headers, + 'type' => $this->type, + ]; + } + + /** + * Setter and getter for the content type + * + * @param string $type + * @return string|self + */ + 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; + } +} diff --git a/kirby/src/Cms/Response.php b/kirby/src/Cms/Response.php new file mode 100755 index 0000000..5eb6d88 --- /dev/null +++ b/kirby/src/Cms/Response.php @@ -0,0 +1,30 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +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 self + */ + public static function redirect(?string $location = null, ?int $code = null) + { + return parent::redirect(Url::to($location ?? '/'), $code); + } +} diff --git a/kirby/src/Cms/Role.php b/kirby/src/Cms/Role.php new file mode 100755 index 0000000..0405c35 --- /dev/null +++ b/kirby/src/Cms/Role.php @@ -0,0 +1,204 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Role extends Model +{ + protected $description; + protected $name; + protected $permissions; + protected $title; + + public function __construct(array $props) + { + $this->setProperties($props); + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } + + public function __toString(): string + { + return $this->name(); + } + + public static function admin(array $inject = []) + { + try { + return static::load('admin'); + } catch (Exception $e) { + return static::factory(static::defaults()['admin'], $inject); + } + } + + 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, + ] + ]; + } + + public function description() + { + return $this->description; + } + + /** + * @param array $props + * @param array $inject + * @return self + */ + public static function factory(array $props, array $inject = []) + { + return new static($props + $inject); + } + + public function id(): string + { + return $this->name(); + } + + public function isAdmin(): bool + { + return $this->name() === 'admin'; + } + + public function isNobody(): bool + { + return $this->name() === 'nobody'; + } + + /** + * @param string $file + * @param array $inject + * @return self + */ + public static function load(string $file, array $inject = []) + { + $data = Data::read($file); + $data['name'] = F::name($file); + + return static::factory($data, $inject); + } + + public function name(): string + { + return $this->name; + } + + /** + * @param array $inject + * @return self + */ + 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; + } + + /** + * @param [type] $description + * @return self + */ + protected function setDescription($description = null) + { + $this->description = I18n::translate($description, $description); + return $this; + } + + /** + * @param string $name + * @return self + */ + protected function setName(string $name) + { + $this->name = $name; + return $this; + } + + /** + * @param [type] $permissions + * @return self + */ + protected function setPermissions($permissions = null) + { + $this->permissions = new Permissions($permissions); + return $this; + } + + /** + * @param [type] $title + * @return self + */ + protected function setTitle($title = null) + { + $this->title = I18n::translate($title, $title); + return $this; + } + + public function title(): string + { + return $this->title = $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(), + ]; + } +} diff --git a/kirby/src/Cms/Roles.php b/kirby/src/Cms/Roles.php new file mode 100755 index 0000000..9095633 --- /dev/null +++ b/kirby/src/Cms/Roles.php @@ -0,0 +1,137 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Roles extends Collection +{ + /** + * Returns a filtered list of all + * roles that can be created by the + * current user + * + * @return self + */ + 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 $this; + } + + /** + * Returns a filtered list of all + * roles that can be created by the + * current user + * + * @return self + */ + 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 $this; + } + + /** + * @param array $roles + * @param array $inject + * @return self + */ + 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); + } + + // always include the admin role + if ($collection->find('admin') === null) { + $collection->set('admin', Role::admin()); + } + + // return the collection sorted by name + return $collection->sortBy('name', 'asc'); + } + + /** + * @param string $root + * @param array $inject + * @return self + */ + public static function load(string $root = null, array $inject = []) + { + $roles = new static(); + + // load roles from plugins + foreach (App::instance()->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); + } + + $roles->set($role->id(), $role); + } + + // load roles from directory + if ($root !== null) { + foreach (glob($root . '/*.yml') as $file) { + $filename = basename($file); + + if ($filename === 'default.yml') { + continue; + } + + $role = Role::load($file, $inject); + $roles->set($role->id(), $role); + } + } + + // always include the admin role + if ($roles->find('admin') === null) { + $roles->set('admin', Role::admin($inject)); + } + + // return the collection sorted by name + return $roles->sortBy('name', 'asc'); + } +} diff --git a/kirby/src/Cms/S.php b/kirby/src/Cms/S.php new file mode 100755 index 0000000..8830077 --- /dev/null +++ b/kirby/src/Cms/S.php @@ -0,0 +1,26 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class S extends Facade +{ + /** + * @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 new file mode 100755 index 0000000..77d3b4b --- /dev/null +++ b/kirby/src/Cms/Search.php @@ -0,0 +1,149 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Search +{ + /** + * @param string $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 Collection $collection + * @param string $query + * @param mixed $params + */ + public static function collection(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, '|')]; + } + + $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 = strtolower($query); + + 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); + + $preg = '!(' . implode('|', $searchwords) . ')!i'; + $results = $collection->filter(function ($item) use ($query, $preg, $options, $lowerQuery) { + $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 (empty($options['fields']) === false) { + $fields = array_map('strtolower', $options['fields']); + $keys = array_intersect($keys, $fields); + } + + $item->searchHits = 0; + $item->searchScore = 0; + + foreach ($keys as $key) { + $score = $options['score'][$key] ?? 1; + $value = $data[$key] ?? (string)$item->$key(); + + $lowerValue = strtolower($value); + + // check for exact matches + if ($lowerQuery == $lowerValue) { + $item->searchScore += 16 * $score; + $item->searchHits += 1; + + // check for exact beginning matches + } elseif (Str::startsWith($lowerValue, $lowerQuery) === true) { + $item->searchScore += 8 * $score; + $item->searchHits += 1; + + // check for exact query matches + } elseif ($matches = preg_match_all('!' . preg_quote($query) . '!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; + } + } + + return $item->searchHits > 0 ? true : false; + }); + + return $results->sortBy('searchScore', 'desc'); + } + + /** + * @param string $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 $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 new file mode 100755 index 0000000..6e7abbc --- /dev/null +++ b/kirby/src/Cms/Section.php @@ -0,0 +1,81 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Section extends Component +{ + /** + * Registry for all component mixins + * + * @var array + */ + public static $mixins = []; + + /** + * Registry for all component types + * + * @var array + */ + public static $types = []; + + + public function __construct(string $type, array $attrs = []) + { + if (isset($attrs['model']) === false) { + throw new InvalidArgumentException('Undefined section model'); + } + + // use the type as fallback for the name + $attrs['name'] = $attrs['name'] ?? $type; + $attrs['type'] = $type; + + parent::__construct($type, $attrs); + } + + /** + * @return \Kirby\Cms\App + */ + public function kirby() + { + return $this->model->kirby(); + } + + /** + * @return \Kirby\Cms\Model + */ + public function model() + { + return $this->model; + } + + public function toArray(): array + { + $array = parent::toArray(); + + unset($array['model']); + + 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 new file mode 100755 index 0000000..cf1334c --- /dev/null +++ b/kirby/src/Cms/Site.php @@ -0,0 +1,688 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Site extends ModelWithContent +{ + const CLASS_ALIAS = 'site'; + + use SiteActions; + use HasChildren; + use HasFiles; + use HasMethods; + + /** + * The SiteBlueprint object + * + * @var SiteBlueprint + */ + protected $blueprint; + + /** + * The error page object + * + * @var 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 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 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, $arguments); + } + + /** + * 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(), + ]); + } + + /** + * 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); + } + + /** + * Returns an array with all blueprints that are available + * as subpages of the site + * + * @param string $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); + } + + /** + * 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 $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 mixed + */ + 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 $path + * @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 full path without leading slash + * + * @internal + * @return string + */ + public function panelPath(): string + { + return 'site'; + } + + /** + * Returns the url to the editing view + * in the panel + * + * @internal + * @param bool $relative + * @return string + */ + public function panelUrl(bool $relative = false): string + { + if ($relative === true) { + return '/' . $this->panelPath(); + } else { + return $this->kirby()->url('panel') . '/' . $this->panelPath(); + } + } + + /** + * 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->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 $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 self + */ + 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 self + */ + 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 self + */ + protected function setHomePageId(string $id = 'home') + { + $this->homePageId = $id; + return $this; + } + + /** + * Sets the current page object + * + * @internal + * @param \Kirby\Cms\Page|null $page + * @return self + */ + public function setPage(Page $page = null) + { + $this->page = $page; + return $this; + } + + /** + * Sets the Url + * + * @param string $url + * @return self + */ + protected function setUrl($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($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 $languageCode + * @param array $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); + } +} diff --git a/kirby/src/Cms/SiteActions.php b/kirby/src/Cms/SiteActions.php new file mode 100755 index 0000000..db759cf --- /dev/null +++ b/kirby/src/Cms/SiteActions.php @@ -0,0 +1,93 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +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(); + + $this->rules()->$action(...$arguments); + $kirby->trigger('site.' . $action . ':before', ...$arguments); + $result = $callback(...$arguments); + $kirby->trigger('site.' . $action . ':after', $result, $old); + $kirby->cache('pages')->flush(); + return $result; + } + + /** + * Change the site title + * + * @param string $title + * @param string|null $languageCode + * @return self + */ + public function changeTitle(string $title, string $languageCode = null) + { + return $this->commit('changeTitle', [$this, $title, $languageCode], 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, + ]); + + return Page::create($props); + } + + /** + * Clean internal caches + * + * @return self + */ + public function purge() + { + $this->blueprint = null; + $this->children = null; + $this->content = null; + $this->files = null; + $this->inventory = null; + $this->translations = null; + + return $this; + } +} diff --git a/kirby/src/Cms/SiteBlueprint.php b/kirby/src/Cms/SiteBlueprint.php new file mode 100755 index 0000000..72d84be --- /dev/null +++ b/kirby/src/Cms/SiteBlueprint.php @@ -0,0 +1,60 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +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); + + // normalize all available page options + $this->props['options'] = $this->normalizeOptions( + $props['options'] ?? true, + // defaults + [ + 'changeTitle' => null, + 'update' => null, + ], + // aliases + [ + 'title' => 'changeTitle', + ] + ); + } + + /** + * Returns the preview settings + * The preview setting controlls 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); + } + + return $preview; + } +} diff --git a/kirby/src/Cms/SitePermissions.php b/kirby/src/Cms/SitePermissions.php new file mode 100755 index 0000000..aa61ea8 --- /dev/null +++ b/kirby/src/Cms/SitePermissions.php @@ -0,0 +1,17 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class SitePermissions extends ModelPermissions +{ + protected $category = 'site'; +} diff --git a/kirby/src/Cms/SiteRules.php b/kirby/src/Cms/SiteRules.php new file mode 100755 index 0000000..8fd1d03 --- /dev/null +++ b/kirby/src/Cms/SiteRules.php @@ -0,0 +1,41 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class SiteRules +{ + 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']); + } + + return true; + } + + public static function update(Site $site, array $content = []): bool + { + if ($site->permissions()->update() !== true) { + throw new PermissionException(['key' => 'site.update.permission']); + } + + return true; + } +} diff --git a/kirby/src/Cms/Structure.php b/kirby/src/Cms/Structure.php new file mode 100755 index 0000000..f1c3602 --- /dev/null +++ b/kirby/src/Cms/Structure.php @@ -0,0 +1,63 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Structure extends Collection +{ + /** + * Creates a new Collection with the given objects + * + * @param array $objects + * @param object $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 + */ + 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'); + } + + $object = new StructureObject([ + 'content' => $props, + 'id' => $props['id'] ?? $id, + 'parent' => $this->parent, + 'structure' => $this + ]); + } + + return parent::__set($object->id(), $object); + } +} diff --git a/kirby/src/Cms/StructureObject.php b/kirby/src/Cms/StructureObject.php new file mode 100755 index 0000000..e04a78a --- /dev/null +++ b/kirby/src/Cms/StructureObject.php @@ -0,0 +1,210 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class StructureObject extends Model +{ + use HasSiblings; + + /** + * The content + * + * @var Content + */ + protected $content; + + /** + * @var string + */ + protected $id; + + /** + * @var Page|Site|File|User + */ + protected $parent; + + /** + * 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; + } + + return $this->content()->get($method, $arguments); + } + + /** + * 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; + } + + if (is_array($this->content) !== true) { + $this->content = []; + } + + return $this->content = new Content($this->content, $this->parent()); + } + + /** + * 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; + } + + return $this === $structure; + } + + /** + * 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 self + */ + 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 self + */ + protected function setId(string $id) + { + $this->id = $id; + return $this; + } + + /** + * Sets the parent Model. This can either be a + * Page, Site, File or User object + * + * @param \Kirby\Cms\Model|null $parent + * @return self + */ + protected function setParent(Model $parent = null) + { + $this->parent = $parent; + return $this; + } + + /** + * Sets the parent Structure collection + * + * @param \Kirby\Cms\Structure $structure + * @return self + */ + 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; + } + + /** + * 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); + + return $array; + } +} diff --git a/kirby/src/Cms/System.php b/kirby/src/Cms/System.php new file mode 100755 index 0000000..79d400f --- /dev/null +++ b/kirby/src/Cms/System.php @@ -0,0 +1,465 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class System +{ + /** + * @var 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(); + } + + /** + * 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(), + ]; + } + + /** + * 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 + { + $url = $this->app->url('index'); + + if (Url::isAbsolute($url)) { + $uri = Url::toObject($url); + } else { + // index URL was configured without host, use the current host + $uri = Uri::current([ + 'path' => $url, + 'query' => null + ]); + } + + return $uri->setScheme(null)->setSlash(false)->toString(); + } + + /** + * Create the most important folders + * if they don't exist yet + * + * @return void + */ + 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 /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(); + $host = $server->host(); + + if ($host === 'localhost') { + return true; + } + + if (in_array($server->address(), ['::1', '127.0.0.1', '0.0.0.0']) === true) { + return true; + } + + if (Str::endsWith($host, '.dev') === true) { + return true; + } + + if (Str::endsWith($host, '.local') === true) { + return true; + } + + if (Str::endsWith($host, '.test') === true) { + 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; + } + + /** + * 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; + } + + /** + * Loads the license file and returns + * the license information if available + * + * @return string|false + */ + public function license() + { + return true; + try { + $license = Json::read($this->app->root('config') . '/.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; + } + + return $license['license']; + } + + /** + * 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(phpversion(), '7.1.0', '>='); + } + + /** + * Validates the license key + * and adds it to the .license file in the config + * folder if possible. + * + * @param string $license + * @param string $email + * @return bool + */ + 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' + ]); + } + + $response = Remote::get('https://licenses.getkirby.com/register', [ + 'data' => [ + 'license' => $license, + 'email' => $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('config') . '/.license'; + + // save the license information + Json::write($file, $json); + + if ($this->license() === false) { + throw new InvalidArgumentException([ + 'key' => 'license.verification' + ]); + } + + return true; + } + + /** + * Check for a valid server environment + * + * @return bool + */ + public function server(): bool + { + $servers = [ + 'apache', + 'caddy', + 'litespeed', + 'nginx', + 'php' + ]; + + $software = $_SERVER['SERVER_SOFTWARE'] ?? null; + + return preg_match('!(' . implode('|', $servers) . ')!i', $software) > 0; + } + + /** + * Check for a writable sessions folder + * + * @return bool + */ + public function sessions(): bool + { + return is_writable($this->app->root('sessions')); + } + + /** + * Return the status as array + * + * @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 new file mode 100755 index 0000000..b7df18e --- /dev/null +++ b/kirby/src/Cms/Template.php @@ -0,0 +1,201 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Template +{ + /** + * Global template data + * + * @var array + */ + public static $data = []; + + /** + * The name of the template + * + * @var string + */ + protected $name; + + /** + * Template type (html, json, etc.) + * + * @var string + */ + protected $type; + + /** + * Default template type if no specific type is set + * + * @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; + } + + /** + * 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 + { + return file_exists($this->file()); + } + + /** + * 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 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 + } + + // Look for the default template provided by an extension. + $path = App::instance()->extension($this->store(), $this->name()); + + if ($path !== null) { + return $path; + } + } + + $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); + } + } + + /** + * 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); + } + + /** + * 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; + } + + /** + * Checks if the template uses the default type + * + * @return bool + */ + public function hasDefaultType(): bool + { + $type = $this->type(); + + return $type === null || $type === $this->defaultType(); + } +} diff --git a/kirby/src/Cms/Translation.php b/kirby/src/Cms/Translation.php new file mode 100755 index 0000000..66efb3d --- /dev/null +++ b/kirby/src/Cms/Translation.php @@ -0,0 +1,193 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Translation +{ + /** + * @var string + */ + protected $code; + + /** + * @var array + */ + protected $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(); + } + + /** + * 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 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; + } + + // get the fallback array + $fallback = App::instance()->translation('en')->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 a single translation + * string by key + * + * @param string $key + * @param string $default + * @return void + */ + 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; + } + + /** + * Loads the translation from the + * json file in Kirby's translations folder + * + * @param string $code + * @param string $root + * @param array $inject + * @return self + */ + public static function load(string $code, string $root, array $inject = []) + { + try { + return new Translation($code, array_merge(Data::read($root), $inject)); + } catch (Exception $e) { + return new Translation($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); + } + + /** + * 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(), + ]; + } +} diff --git a/kirby/src/Cms/Translations.php b/kirby/src/Cms/Translations.php new file mode 100755 index 0000000..468f782 --- /dev/null +++ b/kirby/src/Cms/Translations.php @@ -0,0 +1,70 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Translations extends Collection +{ + public function start(string $code): void + { + F::move($this->parent->contentFile('', true), $this->parent->contentFile($code, true)); + } + + public function stop(string $code): void + { + F::move($this->parent->contentFile($code, true), $this->parent->contentFile('', true)); + } + + /** + * @param array $translations + * @return self + */ + public static function factory(array $translations) + { + $collection = new static(); + + foreach ($translations as $code => $props) { + $translation = new Translation($code, $props); + $collection->data[$translation->code()] = $translation; + } + + return $collection; + } + + /** + * @param string $root + * @param array $inject + * @return self + */ + public static function load(string $root, array $inject = []) + { + $collection = new static(); + + foreach (Dir::read($root) as $filename) { + if (F::extension($filename) !== 'json') { + continue; + } + + $locale = F::name($filename); + $translation = Translation::load($locale, $root . '/' . $filename, $inject[$locale] ?? []); + + $collection->data[$locale] = $translation; + } + + return $collection; + } +} diff --git a/kirby/src/Cms/Url.php b/kirby/src/Cms/Url.php new file mode 100755 index 0000000..c9163fe --- /dev/null +++ b/kirby/src/Cms/Url.php @@ -0,0 +1,96 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Url extends BaseUrl +{ + public static $home = null; + + /** + * 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; + + return file_exists($file) === true ? $url : null; + } + + /** + * Smart resolver for internal and external urls + * + * @param string $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(); + $language = 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 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 (isset($parts[1]) === true) { + $path .= '#' . $parts[1]; + } + } + } + + return $kirby->component('url')($kirby, $path, $options, function (string $path = null, $options = null) { + return parent::to($path, $options); + }); + } +} diff --git a/kirby/src/Cms/User.php b/kirby/src/Cms/User.php new file mode 100755 index 0000000..cc54e71 --- /dev/null +++ b/kirby/src/Cms/User.php @@ -0,0 +1,896 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class User extends ModelWithContent +{ + const CLASS_ALIAS = 'user'; + + use HasFiles; + use HasMethods; + use HasSiblings; + use UserActions; + + /** + * @var File + */ + protected $avatar; + + /** + * @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 string + */ + 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, $arguments); + } + + /** + * Creates a new User object + * + * @param array $props + */ + public function __construct(array $props) + { + $props['id'] = $props['id'] ?? $this->createId(); + $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\UserBlueprint + */ + 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 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->credentials ?? $this->readCredentials(); + } + + /** + * Returns the user email address + * + * @return string + */ + public function email(): ?string + { + return $this->email = $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 self + */ + 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()->filterBy('role', 'admin')->count() <= 1; + } + + /** + * Checks if the user is the last user + * + * @return bool + */ + public function isLastUser(): bool + { + return $this->kirby()->users()->count() === 1; + } + + /** + * Returns the user language + * + * @return string + */ + public function language(): string + { + return $this->language ?? $this->language = $this->credentials()['language'] ?? $this->kirby()->option('panel.language', 'en'); + } + + /** + * Logs the user in + * + * @param string $password + * @param \Kirby\Session\Session|array $session Session options or session object to set the user in + * @return bool + * + * @throws \Kirby\Exception\PermissionException If the password is not valid + */ + 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 $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', $this, $session); + + $session->regenerateToken(); // privilege change + $session->data()->set('user.id', $this->id()); + $this->kirby()->auth()->setUser($this); + + $kirby->trigger('user.login:after', $this, $session); + } + + /** + * Logs the user out + * + * @param \Kirby\Session\Session|array $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', $this, $session); + + // remove the user from the session for future requests + $session->data()->remove('user.id'); + + // 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', $this, null); + } else { + // privilege change + $session->regenerateToken(); + + $kirby->trigger('user.logout:after', $this, $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 + * @return int|string + */ + public function modified(string $format = 'U', string $handler = null) + { + $modifiedContent = F::modified($this->contentFile()); + $modifiedIndex = F::modified($this->root() . '/index.php'); + $modifiedTotal = max([$modifiedContent, $modifiedIndex]); + $handler = $handler ?? $this->kirby()->option('date.handler', 'date'); + + return $handler($format, $modifiedTotal); + } + + /** + * 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); + } + + /** + * Create a dummy nobody + * + * @internal + * @return self + */ + public static function nobody() + { + return new static([ + 'email' => 'nobody@getkirby.com', + 'role' => 'nobody' + ]); + } + + /** + * Panel icon definition + * + * @internal + * @param array $params + * @return array + */ + public function panelIcon(array $params = null): array + { + $params['type'] = 'user'; + + return parent::panelIcon($params); + } + + /** + * Returns the image file object based on provided query + * + * @internal + * @param string|null $query + * @return \Kirby\Cms\File|\Kirby\Cms\Asset|null + */ + protected function panelImageSource(string $query = null) + { + if ($query === null) { + return $this->avatar(); + } + + return parent::panelImageSource($query); + } + + /** + * Returns the full path without leading slash + * + * @internal + * @return string + */ + public function panelPath(): string + { + return 'users/' . $this->id(); + } + + /** + * Returns prepared data for the panel user picker + * + * @param array|null $params + * @return array + */ + public function panelPickerData(array $params = null): array + { + $image = $this->panelImage($params['image'] ?? []); + $icon = $this->panelIcon($image); + + return [ + 'icon' => $icon, + 'id' => $this->id(), + 'image' => $image, + 'email' => $this->email(), + 'info' => $this->toString($params['info'] ?? false), + 'link' => $this->panelUrl(true), + 'text' => $this->toString($params['text'] ?? '{{ user.username }}'), + 'username' => $this->username(), + ]; + } + + /** + * Returns the url to the editing view + * in the panel + * + * @internal + * @param bool $relative + * @return string + */ + public function panelUrl(bool $relative = false): string + { + if ($relative === true) { + return '/' . $this->panelPath(); + } else { + return $this->kirby()->url('panel') . '/' . $this->panelPath(); + } + } + + /** + * 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(); + } + + /** + * 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 self + */ + 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 + * @return self + */ + protected function setEmail(string $email = null) + { + if ($email !== null) { + $this->email = strtolower(trim($email)); + } + return $this; + } + + /** + * Sets the user id + * + * @param string $id + * @return self + */ + protected function setId(string $id = null) + { + $this->id = $id; + return $this; + } + + /** + * Sets the user language + * + * @param string $language + * @return self + */ + protected function setLanguage(string $language = null) + { + $this->language = $language !== null ? trim($language) : null; + return $this; + } + + /** + * Sets the user name + * + * @param string $name + * @return self + */ + protected function setName(string $name = null) + { + $this->name = $name !== null ? trim($name) : null; + return $this; + } + + /** + * Sets the user's password hash + * + * @param string $password + * @return self + */ + protected function setPassword(string $password = null) + { + $this->password = $password; + return $this; + } + + /** + * Sets the user role + * + * @param string $role + * @return self + */ + protected function setRole(string $role = null) + { + $this->role = $role !== null ? strtolower(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 + * @return string + */ + public function toString(string $template = null): string + { + if ($template === null) { + $template = $this->email(); + } + + return parent::toString($template); + } + + /** + * 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 + * @return bool + * + * @throws \Kirby\Exception\NotFoundException If the user has no password + * @throws \Kirby\Exception\InvalidArgumentException If the entered password is not valid + * @throws \Kirby\Exception\InvalidArgumentException If the entered password 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.notSame']); + } + + return true; + } +} diff --git a/kirby/src/Cms/UserActions.php b/kirby/src/Cms/UserActions.php new file mode 100755 index 0000000..02d0213 --- /dev/null +++ b/kirby/src/Cms/UserActions.php @@ -0,0 +1,313 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait UserActions +{ + /** + * Changes the user email address + * + * @param string $email + * @return self + */ + public function changeEmail(string $email) + { + return $this->commit('changeEmail', [$this, $email], function ($user, $email) { + $user = $user->clone([ + 'email' => $email + ]); + + $user->updateCredentials([ + 'email' => $email + ]); + + return $user; + }); + } + + /** + * Changes the user language + * + * @param string $language + * @return self + */ + public function changeLanguage(string $language) + { + return $this->commit('changeLanguage', [$this, $language], function ($user, $language) { + $user = $user->clone([ + 'language' => $language, + ]); + + $user->updateCredentials([ + 'language' => $language + ]); + + return $user; + }); + } + + /** + * Changes the screen name of the user + * + * @param string $name + * @return self + */ + public function changeName(string $name) + { + return $this->commit('changeName', [$this, $name], function ($user, $name) { + $user = $user->clone([ + 'name' => $name + ]); + + $user->updateCredentials([ + 'name' => $name + ]); + + return $user; + }); + } + + /** + * Changes the user password + * + * @param string $password + * @return self + */ + public function changePassword(string $password) + { + return $this->commit('changePassword', [$this, $password], function ($user, $password) { + $user = $user->clone([ + 'password' => $password = User::hashPassword($password) + ]); + + $user->writePassword($password); + + return $user; + }); + } + + /** + * Changes the user role + * + * @param string $role + * @return self + */ + public function changeRole(string $role) + { + return $this->commit('changeRole', [$this, $role], function ($user, $role) { + $user = $user->clone([ + 'role' => $role, + ]); + + $user->updateCredentials([ + 'role' => $role + ]); + + 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 + */ + 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(); + + $this->rules()->$action(...$arguments); + $this->kirby()->trigger('user.' . $action . ':before', ...$arguments); + $result = $callback(...$arguments); + $this->kirby()->trigger('user.' . $action . ':after', $result, $old); + $this->kirby()->cache('pages')->flush(); + return $result; + } + + /** + * Creates a new User from the given props and returns a new User object + * + * @param array $props + * @return self + */ + public static function create(array $props = null) + { + $data = $props; + + if (isset($props['password']) === true) { + $data['password'] = User::hashPassword($props['password']); + } + + $props['role'] = $props['model'] = strtolower($props['role'] ?? 'default'); + + $user = User::factory($data); + + // create a form for the user + $form = Form::for($user, [ + 'values' => $props['content'] ?? [] + ]); + + // inject the content + $user = $user->clone(['content' => $form->strings(true)]); + + // run the hook + return $user->commit('create', [$user, $props], function ($user, $props) { + $user->writeCredentials([ + 'email' => $user->email(), + 'language' => $user->language(), + 'name' => $user->name()->value(), + 'role' => $user->role()->id(), + ]); + + $user->writePassword($user->password()); + + // 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); + + // write the user data + return $user->save($user->content()->toArray(), $languageCode); + }); + } + + /** + * Returns a random user id + * + * @return string + */ + public function createId(): string + { + $length = 8; + $id = Str::random($length); + + while ($this->kirby()->users()->has($id)) { + $length++; + $id = Str::random($length); + } + + return $id; + } + + /** + * Deletes the user + * + * @return bool + */ + public function delete(): bool + { + return $this->commit('delete', [$this], function ($user) { + if ($user->exists() === false) { + return true; + } + + // 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'); + } + + // remove the user from users collection + $user->kirby()->users()->remove($user); + + return true; + }); + } + + /** + * Read the account information from disk + * + * @return array + */ + protected function readCredentials(): array + { + if (file_exists($this->root() . '/index.php') === true) { + $credentials = require $this->root() . '/index.php'; + + return is_array($credentials) === false ? [] : $credentials; + } else { + return []; + } + } + + /** + * Reads the user password from disk + * + * @return string|null + */ + protected function readPassword(): ?string + { + return F::read($this->root() . '/.htpasswd'); + } + + /** + * This always merges the existing credentials + * with the given input. + * + * @param array $credentials + * @return bool + */ + protected function updateCredentials(array $credentials): bool + { + 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 password to disk + * + * @param string $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 new file mode 100755 index 0000000..e86c89c --- /dev/null +++ b/kirby/src/Cms/UserBlueprint.php @@ -0,0 +1,41 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class UserBlueprint extends Blueprint +{ + public function __construct(array $props) + { + // normalize and translate the description + $props['description'] = $this->i18n($props['description'] ?? null); + + // 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, + ] + ); + } +} diff --git a/kirby/src/Cms/UserPermissions.php b/kirby/src/Cms/UserPermissions.php new file mode 100755 index 0000000..bc4ef36 --- /dev/null +++ b/kirby/src/Cms/UserPermissions.php @@ -0,0 +1,60 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class UserPermissions extends ModelPermissions +{ + protected $category = 'users'; + + 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'; + } + + protected function canChangeRole(): bool + { + // only one role, makes no sense to change it + if ($this->user->kirby()->roles()->count() < 2) { + return false; + } + + // users who are not admins cannot change their own role + if ($this->user->is($this->model) === true && $this->user->isAdmin() === false) { + return false; + } + + return $this->model->isLastAdmin() !== true; + } + + 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; + } + + return true; + } + + protected function canDelete(): bool + { + return $this->model->isLastAdmin() !== true; + } +} diff --git a/kirby/src/Cms/UserPicker.php b/kirby/src/Cms/UserPicker.php new file mode 100755 index 0000000..98780db --- /dev/null +++ b/kirby/src/Cms/UserPicker.php @@ -0,0 +1,68 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class UserPicker extends Picker +{ + /** + * Extends the basic defaults + * + * @return array + */ + public function defaults(): array + { + $defaults = parent::defaults(); + $defaults['text'] = '{{ user.username }}'; + + return $defaults; + } + + /** + * Search all users for the picker + * + * @return \Kirby\Cms\Users|null + */ + 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'; + } + + // 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'); + } + + // search + $users = $this->search($users); + + // sort + $users = $users->sortBy('username', 'asc'); + + // paginate + return $this->paginate($users); + } +} diff --git a/kirby/src/Cms/UserRules.php b/kirby/src/Cms/UserRules.php new file mode 100755 index 0000000..20ed836 --- /dev/null +++ b/kirby/src/Cms/UserRules.php @@ -0,0 +1,251 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class UserRules +{ + 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); + } + + 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); + } + + 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; + } + + 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); + } + + 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' + ]); + } + + static::validRole($user, $role); + + 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()] + ]); + } + + return true; + } + + 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()); + + if (empty($props['password']) === false) { + static::validPassword($user, $props['password']); + } + + // get the current user if it exists + $currentUser = $user->kirby()->user(); + + // admins are allowed everything + if ($currentUser && $currentUser->isAdmin() === true) { + return true; + } + + // 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' + ]); + } + + // 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; + } + + 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->permissions()->delete() !== true) { + throw new PermissionException([ + 'key' => 'user.delete.permission', + 'data' => ['name' => $user->username()] + ]); + } + + return true; + } + + 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; + } + + 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 ($duplicate) { + throw new DuplicateException([ + 'key' => 'user.duplicate', + 'data' => ['email' => $email] + ]); + } + + return true; + } + + public static function validId(User $user, string $id): bool + { + if ($user->kirby()->users()->find($id)) { + throw new DuplicateException('A user with this id exists'); + } + + return true; + } + + 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; + } + + public static function validPassword(User $user, string $password): bool + { + if (Str::length($password ?? null) < 8) { + throw new InvalidArgumentException([ + 'key' => 'user.password.invalid', + ]); + } + + return true; + } + + 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', + ]); + } +} diff --git a/kirby/src/Cms/Users.php b/kirby/src/Cms/Users.php new file mode 100755 index 0000000..61fafd6 --- /dev/null +++ b/kirby/src/Cms/Users.php @@ -0,0 +1,138 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Users extends Collection +{ + /** + * All registered users methods + * + * @var array + */ + public static $methods = []; + + public function create(array $data) + { + return User::create($data); + } + + /** + * Adds a single user or + * an entire second collection to the + * current collection + * + * @param mixed $object + * @return self + */ + public function add($object) + { + // add a page collection + if (is_a($object, static::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 object + } elseif (is_a($object, 'Kirby\Cms\User') === true) { + $this->__set($object->id(), $object); + } + + 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 self + */ + 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); + } + + 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', strtolower($key)); + } + + return parent::findByKey($key); + } + + /** + * Loads a user from disk by passing the absolute path (root) + * + * @param string $root + * @param array $inject + * @return self + */ + public static function load(string $root, array $inject = []) + { + $users = new static(); + + foreach (Dir::read($root) as $userDirectory) { + if (is_dir($root . '/' . $userDirectory) === false) { + continue; + } + + // get role information + if (file_exists($root . '/' . $userDirectory . '/index.php') === true) { + $credentials = require $root . '/' . $userDirectory . '/index.php'; + } + + // create user model based on role + $user = User::factory([ + 'id' => $userDirectory, + 'model' => $credentials['role'] ?? null + ] + $inject); + + $users->set($user->id(), $user); + } + + return $users; + } + + /** + * Shortcut for `$users->filterBy('role', 'admin')` + * + * @param string $role + * @return self + */ + public function role(string $role) + { + return $this->filterBy('role', $role); + } +} diff --git a/kirby/src/Cms/Visitor.php b/kirby/src/Cms/Visitor.php new file mode 100755 index 0000000..19eeeb0 --- /dev/null +++ b/kirby/src/Cms/Visitor.php @@ -0,0 +1,25 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Visitor extends Facade +{ + /** + * @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 new file mode 100755 index 0000000..a53a085 --- /dev/null +++ b/kirby/src/Data/Data.php @@ -0,0 +1,125 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Data +{ + /** + * Handler Type Aliases + * + * @var array + */ + public static $aliases = [ + 'md' => 'txt', + 'mdown' => 'txt', + 'yml' => 'yaml', + ]; + + /** + * All registered handlers + * + * @var array + */ + public static $handlers = [ + 'json' => 'Kirby\Data\Json', + 'php' => 'Kirby\Data\PHP', + 'txt' => 'Kirby\Data\Txt', + '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); + + // find a handler or alias + $handler = static::$handlers[$type] ?? + static::$handlers[static::$aliases[$type] ?? null] ?? + null; + + if (class_exists($handler)) { + return new $handler(); + } + + throw new Exception('Missing handler for type: "' . $type . '"'); + } + + /** + * Decodes data with the specified handler + * + * @param string $data + * @param string $type + * @return array + */ + public static function decode(string $data = null, string $type): array + { + return static::handler($type)->decode($data); + } + + /** + * Encodes data with the specified handler + * + * @param array $data + * @param string $type + * @return string + */ + public static function encode(array $data = null, 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); + } + + /** + * Writes data to a file; + * the data handler is automatically chosen by + * the extension if not specified + * + * @param string $file + * @param array $data + * @param string $type + * @return bool + */ + public static function write(string $file = null, array $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 new file mode 100755 index 0000000..6cf6a35 --- /dev/null +++ b/kirby/src/Data/Handler.php @@ -0,0 +1,65 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +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 string $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; + + /** + * 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 static::decode(F::read($file)); + } + + /** + * Writes data to a file + * + * @param string $file + * @param array $data + * @return bool + */ + public static function write(string $file = null, array $data = []): bool + { + return F::write($file, static::encode($data)); + } +} diff --git a/kirby/src/Data/Json.php b/kirby/src/Data/Json.php new file mode 100755 index 0000000..88d741f --- /dev/null +++ b/kirby/src/Data/Json.php @@ -0,0 +1,45 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +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); + } + + /** + * Parses an encoded JSON string and returns a multi-dimensional array + * + * @param string $json + * @return array + */ + public static function decode($json): array + { + $result = json_decode($json, true); + + if (is_array($result) === true) { + return $result; + } else { + throw new Exception('JSON string is invalid'); + } + } +} diff --git a/kirby/src/Data/PHP.php b/kirby/src/Data/PHP.php new file mode 100755 index 0000000..93f8871 --- /dev/null +++ b/kirby/src/Data/PHP.php @@ -0,0 +1,93 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +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, $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 "); + } + + return "[\n" . implode(",\n", $array) . "\n" . $indent . ']'; + case 'boolean': + return $data ? 'true' : 'false'; + case 'int': + case 'double': + return $data; + default: + return var_export($data, true); + } + } + + /** + * PHP arrays don't have to be decoded + * + * @param array $array + * @return array + */ + public static function decode($array): array + { + return $array; + } + + /** + * 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)(include $file); + } + + /** + * Creates a PHP file with the given data + * + * @param string $file + * @param array $data + * @return bool + */ + public static function write(string $file = null, array $data = []): bool + { + $php = static::encode($data); + $php = " + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Txt extends Handler +{ + /** + * Converts an array to an encoded Kirby txt string + * + * @param mixed $data + * @return string + */ + public static function encode($data): string + { + $result = []; + + foreach ((array)$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); + } + + 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 = Yaml::encode($value); + // 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); + + 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 . ':'; + + // multi-line content + if (preg_match('!\R!', $value) === 1) { + $result .= "\n\n"; + } else { + $result .= ' '; + } + + $result .= trim($value); + + return $result; + } + + /** + * Parses a Kirby txt string and returns a multi-dimensional array + * + * @param string $string + * @return array + */ + public static function decode($string): array + { + // 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)))); + + // Don't add fields with empty keys + if (empty($key) === true) { + continue; + } + + $value = trim(substr($field, $pos + 1)); + + // unescape escaped dividers within a field + $data[$key] = preg_replace('!(?<=\n|^)\\\\----!', '----', $value); + } + + return $data; + } +} diff --git a/kirby/src/Data/Yaml.php b/kirby/src/Data/Yaml.php new file mode 100755 index 0000000..0b5f321 --- /dev/null +++ b/kirby/src/Data/Yaml.php @@ -0,0 +1,70 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Yaml extends Handler +{ + /** + * Converts an array to an encoded YAML string + * + * @param mixed $data + * @return string + */ + public static function encode($data): string + { + // 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'); + + // $data, $indent, $wordwrap, $no_opening_dashes + $yaml = Spyc::YAMLDump($data, false, false, true); + + // restore the previous locale settings + setlocale(LC_NUMERIC, $locale); + + return $yaml; + } + + /** + * Parses an encoded YAML string and returns a multi-dimensional array + * + * @param string $yaml + * @return array + */ + public static function decode($yaml): array + { + if ($yaml === null) { + return []; + } + + if (is_array($yaml) === true) { + return $yaml; + } + + // remove BOM + $yaml = str_replace("\xEF\xBB\xBF", '', $yaml); + $result = Spyc::YAMLLoadString($yaml); + + 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 Exception('YAML string is invalid'); // @codeCoverageIgnore + } + } +} diff --git a/kirby/src/Database/Database.php b/kirby/src/Database/Database.php new file mode 100755 index 0000000..1de9bd2 --- /dev/null +++ b/kirby/src/Database/Database.php @@ -0,0 +1,641 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +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; + + /** + * Whitelists for table names + * + * @var array|null + */ + protected $tableWhitelist; + + /** + * 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 instance + * + * @param string $id + * @return self + */ + 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 \Kirby\Database\Database + */ + 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); + + // store the connection + static::$connections[$this->id] = $this; + + // return the connection + return $this->connection; + } + + /** + * Returns the currently active connection + * + * @return \Kirby\Database\Database|null + */ + public function connection() + { + return $this->connection; + } + + /** + * Sets the exception mode for the next query + * + * @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 $data + * @return array + */ + public function trace($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 = $this->connection->lastInsertId(); + $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 + ]); + + // reset some stuff + $this->fail = false; + + // return true or false on success or failure + return $this->lastError === null; + } + + /** + * Exectues 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 + $flags = $options['fetch'] === 'array' ? PDO::FETCH_ASSOC : 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'] === 'array') { + $this->statement->setFetchMode($flags); + } else { + $this->statement->setFetchMode($flags, $options['fetch']); + } + + // fetch that stuff + $results = $this->statement->{$options['method']}(); + + 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->tableWhitelist === null) { + // Get the table whitelist from the database + $sql = $this->sql()->tables($this->database); + $results = $this->query($sql['query'], $sql['bindings']); + + if ($results) { + $this->tableWhitelist = $results->pluck('name'); + } else { + return false; + } + } + + return in_array($table, $this->tableWhitelist) === 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; + } + } + + return true; + } + + /** + * Drops a table + * + * @param string $table + * @return bool + */ + public function dropTable($table): bool + { + $sql = $this->sql()->dropTable($table); + return $this->execute($sql['query'], $sql['bindings']); + } + + /** + * 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 + */ + 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'); + } + + if (isset($params['database']) === false) { + throw new InvalidArgumentException('The mysql connection requires a "database" parameter'); + } + + $parts = []; + + if (empty($params['host']) === false) { + $parts[] = 'host=' . $params['host']; + } + + if (empty($params['port']) === false) { + $parts[] = 'port=' . $params['port']; + } + + if (empty($params['socket']) === false) { + $parts[] = 'unix_socket=' . $params['socket']; + } + + if (empty($params['database']) === false) { + $parts[] = 'dbname=' . $params['database']; + } + + $parts[] = 'charset=' . ($params['charset'] ?? 'utf8'); + + 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'); + } + + return 'sqlite:' . $params['database']; + } +]; diff --git a/kirby/src/Database/Db.php b/kirby/src/Database/Db.php new file mode 100755 index 0000000..f8a7cef --- /dev/null +++ b/kirby/src/Database/Db.php @@ -0,0 +1,270 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Db +{ + const ERROR_UNKNOWN_METHOD = 0; + + /** + * Query shortcuts + * + * @var array + */ + public static $queries = []; + + /** + * The singleton Database object + * + * @var Database + */ + public static $connection = null; + + /** + * (Re)connect the database + * + * @param array $params Pass [] to use the default params from the config + * @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 = $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', ''), + ]; + + return static::$connection = new Database($params); + } + + /** + * Returns the current database connection + * + * @return \Kirby\Database\Database + */ + 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($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 no set of results (i.e. update, insert, delete) + * + * @param string $query + * @param array $bindings + * @return mixed + */ + public static function execute(string $query, array $bindings = []) + { + $db = static::connect(); + return $db->execute($query, $bindings); + } + + /** + * Magic calls for other static db methods, + * which are redircted to the database class if available + * + * @param string $method + * @param mixed $arguments + * @return mixed + */ + public static function __callStatic($method, $arguments) + { + if (isset(static::$queries[$method])) { + return static::$queries[$method](...$arguments); + } + + if (is_callable([static::$connection, $method]) === true) { + return call_user_func_array([static::$connection, $method], $arguments); + } + + throw new InvalidArgumentException('Invalid static Db method: ' . $method, static::ERROR_UNKNOWN_METHOD); + } +} + +/** + * Shortcut for select clauses + * + * @param string $table The name of the table, which should be queried + * @param mixed $columns Either a string with columns or an array of column names + * @param mixed $where The where clause. Can be a string or an array + * @param string $order + * @param int $offset + * @param int $limit + * @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(); +}; + +/** + * Shortcut for selecting a single row in a table + * + * @param string $table The name of the table, which should be queried + * @param mixed $columns Either a string with columns or an array of column names + * @param mixed $where The where clause. Can be a string or an array + * @param string $order + * @param int $offset + * @param int $limit + * @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(); +}; + +/** + * Returns only values from a single column + * + * @param string $table The name of the table, which should be queried + * @param string $column The name of the column to select from + * @param mixed $where The where clause. Can be a string or an array + * @param string $order + * @param int $offset + * @param int $limit + * @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); +}; + +/** + * Shortcut for inserting a new row into a table + * + * @param string $table The name of the table, which should be queried + * @param array $values An array of values, which should be inserted + * @return bool + */ +Db::$queries['insert'] = function (string $table, array $values) { + return Db::table($table)->insert($values); +}; + +/** + * Shortcut for updating a row in a table + * + * @param string $table The name of the table, which should be queried + * @param array $values An array of values, which should be inserted + * @param mixed $where An optional where clause + * @return bool + */ +Db::$queries['update'] = function (string $table, array $values, $where = null) { + return Db::table($table)->where($where)->update($values); +}; + +/** + * Shortcut for deleting rows in a table + * + * @param string $table The name of the table, which should be queried + * @param mixed $where An optional where clause + * @return bool + */ +Db::$queries['delete'] = function (string $table, $where = null) { + return Db::table($table)->where($where)->delete(); +}; + +/** + * Shortcut for counting rows in a table + * + * @param string $table The name of the table, which should be queried + * @param mixed $where An optional where clause + * @return int + */ +Db::$queries['count'] = function (string $table, $where = null) { + return Db::table($table)->where($where)->count(); +}; + +/** + * Shortcut for calculating the minimum value in a column + * + * @param string $table The name of the table, which should be queried + * @param string $column The name of the column of which the minimum should be calculated + * @param mixed $where An optional where clause + * @return mixed + */ +Db::$queries['min'] = function (string $table, string $column, $where = null) { + return Db::table($table)->where($where)->min($column); +}; + +/** + * Shortcut for calculating the maximum value in a column + * + * @param string $table The name of the table, which should be queried + * @param string $column The name of the column of which the maximum should be calculated + * @param mixed $where An optional where clause + * @return mixed + */ +Db::$queries['max'] = function (string $table, string $column, $where = null) { + return Db::table($table)->where($where)->max($column); +}; + +/** + * Shortcut for calculating the average value in a column + * + * @param string $table The name of the table, which should be queried + * @param string $column The name of the column of which the average should be calculated + * @param mixed $where An optional where clause + * @return mixed + */ +Db::$queries['avg'] = function (string $table, string $column, $where = null) { + return Db::table($table)->where($where)->avg($column); +}; + +/** + * Shortcut for calculating the sum of all values in a column + * + * @param string $table The name of the table, which should be queried + * @param string $column The name of the column of which the sum should be calculated + * @param mixed $where An optional where clause + * @return mixed + */ +Db::$queries['sum'] = function (string $table, string $column, $where = null) { + return Db::table($table)->where($where)->sum($column); +}; diff --git a/kirby/src/Database/Query.php b/kirby/src/Database/Query.php new file mode 100755 index 0000000..c1e6684 --- /dev/null +++ b/kirby/src/Database/Query.php @@ -0,0 +1,1061 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Query +{ + const ERROR_INVALID_QUERY_METHOD = 0; + + /** + * Parent Database object + * + * @var Database + */ + protected $database = null; + + /** + * The object which should be fetched for each row + * + * @var string + */ + 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 + * + * @param string $fetch + * @return \Kirby\Database\Query + */ + public function fetch(string $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 + */ + 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 object + */ + 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'); + } + + /** + * 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|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 $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 $order + * @return \Kirby\Database\Query + */ + public function order(string $order = null) + { + $this->order = $order; + return $this; + } + + /** + * Sets the offset for select clauses + * + * @param int $offset + * @return \Kirby\Database\Query + */ + public function offset(int $offset = null) + { + $this->offset = $offset; + return $this; + } + + /** + * Sets the limit for select clauses + * + * @param int $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 string The final query + */ + public function build($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 \Kirby\Database\Query + */ + public function count() + { + return $this->aggregate('COUNT'); + } + + /** + * Builds a max query + * + * @param string $column + * @return \Kirby\Database\Query + */ + public function max(string $column) + { + return $this->aggregate('MAX', $column); + } + + /** + * Builds a min query + * + * @param string $column + * @return \Kirby\Database\Query + */ + public function min(string $column) + { + return $this->aggregate('MIN', $column); + } + + /** + * Builds a sum query + * + * @param string $column + * @return \Kirby\Database\Query + */ + public function sum(string $column) + { + return $this->aggregate('SUM', $column); + } + + /** + * Builds an average query + * + * @param string $column + * @return \Kirby\Database\Query + */ + public function avg(string $column) + { + return $this->aggregate('AVG', $column); + } + + /** + * Builds an aggregation query. + * This is used by all the aggregation methods above + * + * @param string $method + * @param string $column + * @param string $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'], $params); + + $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($column) + { + $sql = $this->database->sql(); + $primaryKey = $sql->combineIdentifier($this->table, $this->primaryKeyName); + + $results = $this->query($this->select([$column])->order($primaryKey . ' ASC')->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($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 array $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 array $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 string $current Current value (like $this->where) + * @return string + */ + protected function filterQuery($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']); + if (is_array($args[2]) === true) { + $predicate = trim(strtoupper($args[1])); + + 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) . ')'; + + $this->bindings($bindings); + + // ->where('username', 'like', 'myuser'); + } else { + $predicate = trim(strtoupper($args[1])); + $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 new file mode 100755 index 0000000..09e18fc --- /dev/null +++ b/kirby/src/Database/Sql.php @@ -0,0 +1,933 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Sql +{ + /** + * List of literals which should not be escaped in queries + * + * @var array + */ + public static $literals = ['NOW()', null]; + + /** + * The parent database connection + * + * @var Database + */ + public $database; + + /** + * Constructor + * + * @param \Kirby\Database\Database $database + */ + public function __construct($database) + { + $this->database = $database; + } + + /** + * Returns a randomly generated binding name + * + * @param string $label String that contains lowercase letters and numbers to use as a readable identifier + * @param string $prefix + * @return string + */ + public function bindingName(string $label): string + { + // make sure that the binding name is valid to prevent injections + if (!preg_match('/^[a-z0-9_]+$/', $label)) { + $label = 'invalid'; + } + + return ':' . $label . '_' . Str::random(16); + } + + /** + * Returns a list of columns for a specified table + * MySQL version + * + * @param string $table The 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; + + return [ + 'query' => $query, + 'bindings' => [ + $databaseBinding => $this->database->name(), + $tableBinding => $table, + ] + ]; + } + + /** + * Optionl default value definition for the column + * + * @param array $column + * @return array + */ + public function columnDefault(array $column): array + { + if (isset($column['default']) === false) { + return [ + 'query' => null, + 'bindings' => [] + ]; + } + + $binding = $this->bindingName($column['name'] . '_default'); + + return [ + 'query' => 'DEFAULT ' . $binding, + 'bindings' => [ + $binding = $column['default'] + ] + ]; + } + + /** + * Returns a valid column name + * + * @param string $table + * @param string $column + * @param bool $enforceQualified + * @return string|null + */ + public function columnName(string $table, string $column, bool $enforceQualified = false): ?string + { + list($table, $column) = $this->splitIdentifier($table, $column); + + if ($this->validateColumn($table, $column) === true) { + return $this->combineIdentifier($table, $column, $enforceQualified !== true); + } + + return null; + } + + /** + * Abstracted column types to simplify table + * creation for multiple database drivers + * + * @return array + */ + public function columnTypes(): array + { + return [ + 'id' => '{{ name }} INT(11) UNSIGNED NOT NULL AUTO_INCREMENT', + 'varchar' => '{{ name }} varchar(255) {{ null }} {{ default }}', + 'text' => '{{ name }} TEXT', + 'int' => '{{ name }} INT(11) UNSIGNED {{ null }} {{ default }}', + 'timestamp' => '{{ name }} TIMESTAMP {{ null }} {{ default }}' + ]; + } + + /** + * Optional key definition for the column. + * + * @param array $column + * @return array + */ + public function columnKey(array $column): array + { + return [ + 'query' => null, + 'bindings' => [] + ]; + } + + /** + * Combines an identifier (table and column) + * Default version for MySQL + * + * @param $table string + * @param $column string + * @param $values boolean 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 syntax for a single column + * + * @param string $table + * @param array $column + * @return array + */ + public function createColumn(string $table, array $column): array + { + // column type + if (isset($column['type']) === false) { + throw new InvalidArgumentException('No column type given for column ' . $column); + } + + // column name + if (isset($column['name']) === false) { + throw new InvalidArgumentException('No column name given'); + } + + if ($column['type'] === 'id') { + $column['key'] = 'PRIMARY'; + } + + if (!$template = ($this->columnTypes()[$column['type']] ?? null)) { + throw new InvalidArgumentException('Unsupported column type: ' . $column['type']); + } + + // null + if (A::get($column, 'null') === false) { + $null = 'NOT NULL'; + } else { + $null = 'NULL'; + } + + // indexes/keys + $key = false; + + if (isset($column['key']) === true) { + $column['key'] = strtoupper($column['key']); + + // backwards compatibility + if ($column['key'] === 'PRIMARY') { + $column['key'] = 'PRIMARY KEY'; + } + + if (in_array($column['key'], ['PRIMARY KEY', 'INDEX']) === true) { + $key = $column['key']; + } + } + + // default value + $columnDefault = $this->columnDefault($column); + $columnKey = $this->columnKey($column); + + $query = trim(Str::template($template, [ + 'name' => $this->quoteIdentifier($column['name']), + 'null' => $null, + 'key' => $columnKey['query'], + 'default' => $columnDefault['query'], + ])); + + $bindings = array_merge($columnKey['bindings'], $columnDefault['bindings']); + + return [ + 'query' => $query, + 'bindings' => $bindings, + 'key' => $key + ]; + } + + /** + * Creates a table with a simple scheme array for columns + * Default version for MySQL + * + * @param string $table The table name + * @param array $columns + * @return array + */ + public function createTable(string $table, array $columns = []): array + { + $output = []; + $keys = []; + $bindings = []; + + foreach ($columns as $name => $column) { + $sql = $this->createColumn($table, $column); + + $output[] = $sql['query']; + + if ($sql['key']) { + $keys[$column['name']] = $sql['key']; + } + + $bindings = array_merge($bindings, $sql['bindings']); + } + + // combine columns + $inner = implode(',' . PHP_EOL, $output); + + // add keys + foreach ($keys as $name => $key) { + $inner .= ',' . PHP_EOL . $key . ' (' . $this->quoteIdentifier($name) . ')'; + } + + return [ + 'query' => 'CREATE TABLE ' . $this->quoteIdentifier($table) . ' (' . PHP_EOL . $inner . PHP_EOL . ')', + 'bindings' => $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 $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 + */ + 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 $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 = $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) + * Default version for MySQL + * + * @param $identifier string + * @return string + */ + public function quoteIdentifier(string $identifier): string + { + // * is special + if ($identifier === '*') { + return $identifier; + } + + // replace every backtick with two backticks + $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 + */ + 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 list of tables for a specified database + * MySQL version + * + * @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() + ] + ]; + } + + /** + * Validates and quotes a table name + * + * @param string $table + * @return string + */ + 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 + */ + public function validateColumn(string $table, string $column): bool + { + if ($this->database->validateColumn($table, $column) === false) { + 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 + */ + 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 + ]; + } +} diff --git a/kirby/src/Database/Sql/Mysql.php b/kirby/src/Database/Sql/Mysql.php new file mode 100755 index 0000000..c72a4a9 --- /dev/null +++ b/kirby/src/Database/Sql/Mysql.php @@ -0,0 +1,18 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Mysql extends Sql +{ +} diff --git a/kirby/src/Database/Sql/Sqlite.php b/kirby/src/Database/Sql/Sqlite.php new file mode 100755 index 0000000..842aa80 --- /dev/null +++ b/kirby/src/Database/Sql/Sqlite.php @@ -0,0 +1,125 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Sqlite extends Sql +{ + /** + * Returns a list of columns for a specified table + * SQLite version + * + * @param string $table The table name + * @return string + */ + public function columns(string $table): array + { + return [ + 'query' => 'PRAGMA table_info(' . $this->tableName($table) . ')', + 'bindings' => [], + ]; + } + + /** + * Optional key definition for the column. + * + * @param array $column + * @return array + */ + public function columnKey(array $column): array + { + if (isset($column['key']) === false || $column['key'] === 'INDEX') { + return [ + 'query' => null, + 'bindings' => [] + ]; + } + + return [ + 'query' => $column['key'], + 'bindings' => [] + ]; + } + + /** + * Abstracted column types to simplify table + * creation for multiple database drivers + * + * @return array + */ + public function columnTypes(): array + { + return [ + 'id' => '{{ name }} INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL UNIQUE', + 'varchar' => '{{ name }} TEXT {{ null }} {{ key }} {{ default }}', + 'text' => '{{ name }} TEXT {{ null }} {{ key }} {{ default }}', + 'int' => '{{ name }} INTEGER {{ null }} {{ key }} {{ default }}', + 'timestamp' => '{{ name }} INTEGER {{ null }} {{ key }} {{ default }}' + ]; + } + + /** + * Combines an identifier (table and column) + * SQLite version + * + * @param $table string + * @param $column string + * @param $values boolean 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) { + return $this->quoteIdentifier($column); + } + + return $this->quoteIdentifier($table) . '.' . $this->quoteIdentifier($column); + } + + /** + * Quotes an identifier (table *or* column) + * + * @param $identifier string + * @return string + */ + public function quoteIdentifier(string $identifier): string + { + // * is special + if ($identifier === '*') { + return $identifier; + } + + // replace every quote with two quotes + $identifier = str_replace('"', '""', $identifier); + + // wrap in quotes + return '"' . $identifier . '"'; + } + + /** + * Returns a list of tables of the database + * SQLite version + * + * @param string $database The database name + * @return string + */ + public function tables(): array + { + return [ + 'query' => 'SELECT name FROM sqlite_master WHERE type = "table"', + 'bindings' => [] + ]; + } +} diff --git a/kirby/src/Email/Body.php b/kirby/src/Email/Body.php new file mode 100755 index 0000000..bf557e7 --- /dev/null +++ b/kirby/src/Email/Body.php @@ -0,0 +1,51 @@ +, + * Nico Hoffmann + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Body +{ + use Properties; + + protected $html; + protected $text; + + public function __construct(array $props = []) + { + $this->setProperties($props); + } + + public function html() + { + return $this->html; + } + + public function text() + { + return $this->text; + } + + protected function setHtml(string $html = null) + { + $this->html = $html; + 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 new file mode 100755 index 0000000..9b3e53e --- /dev/null +++ b/kirby/src/Email/Email.php @@ -0,0 +1,224 @@ +, + * Nico Hoffmann + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Email +{ + use Properties; + + protected $attachments; + protected $body; + protected $bcc; + protected $cc; + protected $from; + protected $fromName; + protected $replyTo; + protected $replyToName; + protected $isSent = false; + protected $subject; + protected $to; + protected $transport; + + public function __construct(array $props = [], bool $debug = false) + { + $this->setProperties($props); + + if ($debug === false) { + $this->send(); + } + } + + public function attachments(): array + { + return $this->attachments; + } + + /** + * @return \Kirby\Email\Body + */ + public function body() + { + return $this->body; + } + + public function bcc(): array + { + return $this->bcc; + } + + public function cc(): array + { + return $this->cc; + } + + protected function defaultTransport(): array + { + return [ + 'type' => 'mail' + ]; + } + + public function from(): string + { + return $this->from; + } + + public function fromName(): ?string + { + return $this->fromName; + } + + public function isHtml() + { + return $this->body()->html() !== null; + } + + public function isSent(): bool + { + return $this->isSent; + } + + public function replyTo(): string + { + return $this->replyTo; + } + + public function replyToName(): ?string + { + return $this->replyToName; + } + + protected function resolveEmail($email = null, bool $multiple = true) + { + if ($email === null) { + return $multiple === true ? [] : ''; + } + + 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; + } + + // 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]; + } + + public function send(): bool + { + return $this->isSent = true; + } + + protected function setAttachments($attachments = null) + { + $this->attachments = $attachments ?? []; + return $this; + } + + protected function setBody($body) + { + if (is_string($body) === true) { + $body = ['text' => $body]; + } + + $this->body = new Body($body); + return $this; + } + + protected function setBcc($bcc = null) + { + $this->bcc = $this->resolveEmail($bcc); + return $this; + } + + protected function setCc($cc = null) + { + $this->cc = $this->resolveEmail($cc); + return $this; + } + + protected function setFrom(string $from) + { + $this->from = $this->resolveEmail($from, false); + return $this; + } + + protected function setFromName(string $fromName = null) + { + $this->fromName = $fromName; + return $this; + } + + protected function setReplyTo(string $replyTo = null) + { + $this->replyTo = $this->resolveEmail($replyTo, false); + return $this; + } + + protected function setReplyToName(string $replyToName = null) + { + $this->replyToName = $replyToName; + return $this; + } + + protected function setSubject(string $subject) + { + $this->subject = $subject; + return $this; + } + + protected function setTo($to) + { + $this->to = $this->resolveEmail($to); + return $this; + } + + protected function setTransport($transport = null) + { + $this->transport = $transport; + return $this; + } + + public function subject(): string + { + return $this->subject; + } + + public function to(): array + { + return $this->to; + } + + public function transport(): array + { + return $this->transport ?? $this->defaultTransport(); + } +} diff --git a/kirby/src/Email/PHPMailer.php b/kirby/src/Email/PHPMailer.php new file mode 100755 index 0000000..6ab3816 --- /dev/null +++ b/kirby/src/Email/PHPMailer.php @@ -0,0 +1,76 @@ +, + * Nico Hoffmann + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class PHPMailer extends Email +{ + public function send(bool $debug = false): bool + { + $mailer = new Mailer(true); + + // set sender's address + $mailer->setFrom($this->from(), $this->fromName() ?? ''); + + // optional reply-to address + if ($replyTo = $this->replyTo()) { + $mailer->addReplyTo($replyTo, $this->replyToName() ?? ''); + } + + // add (multiple) recepient, 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'; + + // 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); + } + + // 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 ($debug === true) { + return $this->isSent = true; + } + + return $this->isSent = $mailer->send(); + } +} diff --git a/kirby/src/Exception/BadMethodCallException.php b/kirby/src/Exception/BadMethodCallException.php new file mode 100755 index 0000000..1be6fb7 --- /dev/null +++ b/kirby/src/Exception/BadMethodCallException.php @@ -0,0 +1,21 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +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]; +} diff --git a/kirby/src/Exception/DuplicateException.php b/kirby/src/Exception/DuplicateException.php new file mode 100755 index 0000000..7684c4c --- /dev/null +++ b/kirby/src/Exception/DuplicateException.php @@ -0,0 +1,21 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class DuplicateException extends Exception +{ + 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 new file mode 100755 index 0000000..1c1532d --- /dev/null +++ b/kirby/src/Exception/ErrorPageException.php @@ -0,0 +1,21 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class ErrorPageException extends Exception +{ + 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 new file mode 100755 index 0000000..f6bc588 --- /dev/null +++ b/kirby/src/Exception/Exception.php @@ -0,0 +1,221 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Exception extends \Exception +{ + /** + * 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; + + /** + * 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; + + /** + * 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'; + + /** + * 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); + + 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; + + 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; + } + + 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; + } + + // format message with passed data + $message = Str::template($message, $this->data, '-', '{', '}'); + + // handover to Exception parent class constructor + parent::__construct($message, null, $args['previous'] ?? null); + } + + // 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(); + + if (empty($_SERVER['DOCUMENT_ROOT']) === false) { + $file = ltrim(Str::after($file, $_SERVER['DOCUMENT_ROOT']), '/'); + } + + return $file; + } + + /** + * 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 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 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() + ]; + } +} diff --git a/kirby/src/Exception/InvalidArgumentException.php b/kirby/src/Exception/InvalidArgumentException.php new file mode 100755 index 0000000..e536f4f --- /dev/null +++ b/kirby/src/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +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]; +} diff --git a/kirby/src/Exception/LogicException.php b/kirby/src/Exception/LogicException.php new file mode 100755 index 0000000..7d9ac20 --- /dev/null +++ b/kirby/src/Exception/LogicException.php @@ -0,0 +1,20 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class LogicException extends Exception +{ + 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 new file mode 100755 index 0000000..af08c04 --- /dev/null +++ b/kirby/src/Exception/NotFoundException.php @@ -0,0 +1,20 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class NotFoundException extends Exception +{ + 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 new file mode 100755 index 0000000..b53a053 --- /dev/null +++ b/kirby/src/Exception/PermissionException.php @@ -0,0 +1,21 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class PermissionException extends Exception +{ + protected static $defaultKey = 'permission'; + protected static $defaultFallback = 'You are not allowed to do this'; + protected static $defaultHttpCode = 403; +} diff --git a/kirby/src/Form/Field.php b/kirby/src/Form/Field.php new file mode 100755 index 0000000..fddf0e4 --- /dev/null +++ b/kirby/src/Form/Field.php @@ -0,0 +1,340 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Field extends Component +{ + /** + * Registry for all component mixins + * + * @var array + */ + public static $mixins = []; + + /** + * Registry for all component types + * + * @var array + */ + public static $types = []; + + /** + * An array of all found errors + * + * @var array + */ + protected $errors = []; + + public function __construct(string $type, array $attrs = []) + { + 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'); + } + + // use the type as fallback for the name + $attrs['name'] = $attrs['name'] ?? $type; + $attrs['type'] = $type; + + parent::__construct($type, $attrs); + + $this->validate(); + } + + /** + * @return mixed + */ + public function api() + { + if (isset($this->options['api']) === true && is_callable($this->options['api']) === true) { + return $this->options['api']->call($this); + } + } + + /** + * @param mixed $default + * @return mixed + */ + public function data($default = false) + { + $save = $this->options['save'] ?? true; + + if ($default === true && $this->isEmpty($this->value)) { + $value = $this->default(); + } else { + $value = $this->value; + } + + if ($save === false) { + return null; + } elseif (is_callable($save) === true) { + return $save->call($this, $value); + } else { + return $value; + } + } + + 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 () { + if ($this->after !== null) { + return $this->model()->toString($this->after); + } + }, + 'before' => function () { + if ($this->before !== null) { + return $this->model()->toString($this->before); + } + }, + 'default' => function () { + if ($this->default === null) { + return; + } + + if (is_string($this->default) === false) { + return $this->default; + } + + return $this->model()->toString($this->default); + }, + 'help' => function () { + if ($this->help) { + $help = $this->model()->toString($this->help); + $help = $this->kirby()->kirbytext($help); + return $help; + } + }, + 'label' => function () { + if ($this->label !== null) { + return $this->model()->toString($this->label); + } + }, + 'placeholder' => function () { + if ($this->placeholder !== null) { + return $this->model()->toString($this->placeholder); + } + } + ] + ]; + } + + public function errors(): array + { + return $this->errors; + } + + 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); + } + + return in_array($value, [null, '', []], true); + } + + public function isInvalid(): bool + { + return empty($this->errors) === false; + } + + public function isRequired(): bool + { + return $this->required ?? false; + } + + public function isValid(): bool + { + return empty($this->errors) === true; + } + + /** + * @return \Kirby\Cms\App + */ + public function kirby() + { + return $this->model->kirby(); + } + + public function model() + { + return $this->model; + } + + public function save(): bool + { + return ($this->options['save'] ?? true) !== false; + } + + public function toArray(): array + { + $array = parent::toArray(); + + unset($array['model']); + + $array['invalid'] = $this->isInvalid(); + $array['errors'] = $this->errors(); + $array['signature'] = md5(json_encode($array)); + + ksort($array); + + return array_filter($array, function ($item) { + return $item !== null; + }); + } + + protected function validate(): void + { + $validations = $this->options['validations'] ?? []; + $this->errors = []; + + // validate required values + if ($this->isRequired() === true && $this->save() === true && $this->isEmpty() === 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; + } + + 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) { + $errors = V::errors($this->value(), $this->validate); + + if (empty($errors) === false) { + $this->errors = array_merge($this->errors, $errors); + } + } + } + + public function value() + { + return $this->save() ? $this->value : null; + } +} diff --git a/kirby/src/Form/Fields.php b/kirby/src/Form/Fields.php new file mode 100755 index 0000000..6908efd --- /dev/null +++ b/kirby/src/Form/Fields.php @@ -0,0 +1,56 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +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 $field + */ + public function __set(string $name, $field) + { + if (is_array($field)) { + // use the array key as name if the name is not set + $field['name'] = $field['name'] ?? $name; + $field = new Field($field['type'], $field); + } + + return parent::__set($field->name(), $field); + } + + /** + * Converts the fields collection to an + * array and also does that for every + * included field. + * + * @param Closure $map + * @return array + */ + public function toArray(Closure $map = null): array + { + $array = []; + + foreach ($this as $field) { + $array[$field->name()] = $field->toArray(); + } + + return $array; + } +} diff --git a/kirby/src/Form/Form.php b/kirby/src/Form/Form.php new file mode 100755 index 0000000..3dd0410 --- /dev/null +++ b/kirby/src/Form/Form.php @@ -0,0 +1,183 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Form +{ + protected $errors; + protected $fields; + protected $values = []; + + public function __construct(array $props) + { + $fields = $props['fields'] ?? []; + $values = $props['values'] ?? []; + $input = $props['input'] ?? []; + $strict = $props['strict'] ?? false; + $inject = $props; + + // lowercase all value names + $values = array_change_key_case($values); + $input = array_change_key_case($input); + + unset($inject['fields'], $inject['values'], $inject['input']); + + $this->fields = new Fields(); + $this->values = []; + + foreach ($fields as $name => $props) { + + // inject stuff from the form constructor (model, etc.) + $props = array_merge($inject, $props); + + // inject the name + $props['name'] = $name = strtolower($name); + + // 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; + } + + try { + $field = new Field($props['type'], $props); + } catch (Throwable $e) { + $field = static::exceptionField($e, $props); + } + + if ($field->save() !== false) { + $this->values[$name] = $field->value(); + } + + $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); + + foreach ($input as $key => $value) { + if (isset($this->values[$key]) === false) { + $this->values[$key] = $value; + } + } + } + } + + public function data($defaults = false): array + { + $data = $this->values; + + foreach ($this->fields as $field) { + if ($field->save() === false || $field->unset() === true) { + $data[$field->name()] = null; + } else { + $data[$field->name()] = $field->data($defaults); + } + } + + return $data; + } + + public function errors(): array + { + if ($this->errors !== null) { + return $this->errors; + } + + $this->errors = []; + + foreach ($this->fields as $field) { + if (empty($field->errors()) === false) { + $this->errors[$field->name()] = [ + 'label' => $field->label(), + 'message' => $field->errors() + ]; + } + } + + return $this->errors; + } + + public static function exceptionField(Throwable $exception, array $props = []) + { + $props = array_merge($props, [ + 'label' => 'Error in "' . $props['name'] . '" field', + 'theme' => 'negative', + 'text' => strip_tags($exception->getMessage()), + ]); + + return new Field('info', $props); + } + + public function fields() + { + return $this->fields; + } + + public function isInvalid(): bool + { + return empty($this->errors()) === false; + } + + public function isValid(): bool + { + return empty($this->errors()) === true; + } + + public function strings($defaults = false): array + { + $strings = []; + + foreach ($this->data($defaults) as $key => $value) { + if ($value === null) { + $strings[$key] = null; + } elseif (is_array($value) === true) { + $strings[$key] = Yaml::encode($value); + } else { + $strings[$key] = $value; + } + } + + return $strings; + } + + public function toArray(): array + { + $array = [ + 'errors' => $this->errors(), + 'fields' => $this->fields->toArray(function ($item) { + return $item->toArray(); + }), + 'invalid' => $this->isInvalid() + ]; + + return $array; + } + + public function values(): array + { + return $this->values; + } +} diff --git a/kirby/src/Form/Options.php b/kirby/src/Form/Options.php new file mode 100755 index 0000000..42daa2d --- /dev/null +++ b/kirby/src/Form/Options.php @@ -0,0 +1,173 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Options +{ + protected static function aliases(): array + { + return [ + 'Kirby\Cms\File' => 'file', + 'Kirby\Toolkit\Obj' => 'arrayItem', + 'Kirby\Cms\Page' => 'page', + 'Kirby\Cms\StructureObject' => 'structureItem', + 'Kirby\Cms\User' => 'user', + ]; + } + + public static function api($api, $model = null): array + { + $model = $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; + } + + $optionsApi = new OptionsApi([ + 'data' => static::data($model), + 'fetch' => $fetch, + 'url' => $url, + 'text' => $text, + 'value' => $value + ]); + + return $optionsApi->options(); + } + + protected static function data($model): array + { + $kirby = $model->kirby(); + + // 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; + } + } + + return $data; + } + + public static function factory($options, array $props = [], $model = null): array + { + switch ($options) { + case 'api': + $options = static::api($props['api']); + 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 []; + } + + $result = []; + + 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']); + } + + // add the option to the list + $result[] = $option; + } + + return $result; + } + + public static function query($query, $model = null): array + { + $model = $model ?? App::instance()->site(); + + // default text setup + $text = [ + 'arrayItem' => '{{ arrayItem.value }}', + 'file' => '{{ file.filename }}', + 'page' => '{{ page.title }}', + 'structureItem' => '{{ structureItem.title }}', + 'user' => '{{ user.username }}', + ]; + + // default value setup + $value = [ + 'arrayItem' => '{{ arrayItem.value }}', + 'file' => '{{ file.id }}', + 'page' => '{{ page.id }}', + 'structureItem' => '{{ structureItem.id }}', + 'user' => '{{ user.email }}', + ]; + + // resolve array query setup + if (is_array($query) === true) { + $text = $query['text'] ?? $text; + $value = $query['value'] ?? $value; + $query = $query['fetch'] ?? null; + } + + $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 new file mode 100755 index 0000000..bed3771 --- /dev/null +++ b/kirby/src/Form/OptionsApi.php @@ -0,0 +1,135 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class OptionsApi +{ + use Properties; + + protected $data; + protected $fetch; + protected $options; + protected $text = '{{ item.value }}'; + protected $url; + protected $value = '{{ item.key }}'; + + public function __construct(array $props) + { + $this->setProperties($props); + } + + public function data(): array + { + return $this->data; + } + + public function fetch() + { + return $this->fetch; + } + + protected function field(string $field, array $data) + { + $value = $this->$field(); + return Str::template($value, $data); + } + + public function options(): array + { + if (is_array($this->options) === true) { + return $this->options; + } + + $content = @file_get_contents($this->url()); + + if (empty($content) === true) { + return []; + } + + $data = json_decode($content, true); + + if (is_array($data) === false) { + throw new InvalidArgumentException('Invalid options format'); + } + + $result = (new Query($this->fetch(), Nest::create($data)))->result(); + $options = []; + + foreach ($result as $item) { + $data = array_merge($this->data(), ['item' => $item]); + + $options[] = [ + 'text' => $this->field('text', $data), + 'value' => $this->field('value', $data), + ]; + } + + return $options; + } + + protected function setData(array $data) + { + $this->data = $data; + return $this; + } + + protected function setFetch(string $fetch = null) + { + $this->fetch = $fetch; + return $this; + } + + protected function setText($text = null) + { + $this->text = $text; + return $this; + } + + protected function setUrl($url) + { + $this->url = $url; + return $this; + } + + protected function setValue($value = null) + { + $this->value = $value; + return $this; + } + + public function text() + { + return $this->text; + } + + public function toArray(): array + { + return $this->options(); + } + + public function url(): string + { + return Str::template($this->url, $this->data()); + } + + public function value() + { + return $this->value; + } +} diff --git a/kirby/src/Form/OptionsQuery.php b/kirby/src/Form/OptionsQuery.php new file mode 100755 index 0000000..6f536fb --- /dev/null +++ b/kirby/src/Form/OptionsQuery.php @@ -0,0 +1,180 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class OptionsQuery +{ + use Properties; + + protected $aliases = []; + protected $data; + protected $options; + protected $query; + protected $text; + protected $value; + + public function __construct(array $props) + { + $this->setProperties($props); + } + + public function aliases(): array + { + return $this->aliases; + } + + public function data(): array + { + return $this->data; + } + + 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'); + } + + $value = $value[$object]; + } + + return Str::template($value, $data); + } + + 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 = []; + + 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) + ]; + } + + return $this->options = $options; + } + + public function query(): string + { + return $this->query; + } + + 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; + } + } + + return 'item'; + } + + 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); + } + + if (is_a($result, 'Kirby\Toolkit\Collection') === false) { + throw new InvalidArgumentException('Invalid query result data'); + } + + return $result; + } + + protected function setAliases(array $aliases = null) + { + $this->aliases = $aliases; + return $this; + } + + protected function setData(array $data) + { + $this->data = $data; + return $this; + } + + protected function setQuery(string $query) + { + $this->query = $query; + return $this; + } + + protected function setText($text) + { + $this->text = $text; + return $this; + } + + protected function setValue($value) + { + $this->value = $value; + return $this; + } + + public function text() + { + return $this->text; + } + + public function toArray(): array + { + return $this->options(); + } + + public function value() + { + return $this->value; + } +} diff --git a/kirby/src/Form/Validations.php b/kirby/src/Form/Validations.php new file mode 100755 index 0000000..fdc260a --- /dev/null +++ b/kirby/src/Form/Validations.php @@ -0,0 +1,190 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Validations +{ + public static function boolean(Field $field, $value): bool + { + if ($field->isEmpty($value) === false) { + if (is_bool($value) === false) { + throw new InvalidArgumentException([ + 'key' => 'validation.boolean' + ]); + } + } + + return true; + } + + public static function date(Field $field, $value): bool + { + if ($field->isEmpty($value) === false) { + if (V::date($value) !== true) { + throw new InvalidArgumentException( + V::message('date', $value) + ); + } + } + + return true; + } + + public static function email(Field $field, $value): bool + { + if ($field->isEmpty($value) === false) { + if (V::email($value) === false) { + throw new InvalidArgumentException( + V::message('email', $value) + ); + } + } + + return true; + } + + public static function max(Field $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; + } + + public static function maxlength(Field $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; + } + + public static function min(Field $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; + } + + public static function minlength(Field $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; + } + + public static function pattern(Field $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; + } + + public static function required(Field $field, $value): bool + { + if ($field->isRequired() === true && $field->save() === true && $field->isEmpty($value) === true) { + throw new InvalidArgumentException([ + 'key' => 'validation.required' + ]); + } + + return true; + } + + public static function option(Field $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' + ]); + } + } + + return true; + } + + public static function options(Field $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; + } + + public static function time(Field $field, $value): bool + { + if ($field->isEmpty($value) === false) { + if (V::time($value) !== true) { + throw new InvalidArgumentException( + V::message('time', $value) + ); + } + } + + return true; + } + + public static function url(Field $field, $value): bool + { + if ($field->isEmpty($value) === false) { + if (V::url($value) === false) { + throw new InvalidArgumentException( + V::message('url', $value) + ); + } + } + + return true; + } +} diff --git a/kirby/src/Http/Cookie.php b/kirby/src/Http/Cookie.php new file mode 100755 index 0000000..41285e5 --- /dev/null +++ b/kirby/src/Http/Cookie.php @@ -0,0 +1,206 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Cookie +{ + /** + * 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 + * @return bool true: cookie was created, + * false: cookie creation failed + */ + public static function set(string $key, string $value, array $options = []): bool + { + // extract options + $lifetime = $options['lifetime'] ?? 0; + $path = $options['path'] ?? '/'; + $domain = $options['domain'] ?? null; + $secure = $options['secure'] ?? false; + $httpOnly = $options['httpOnly'] ?? true; + + // add an HMAC signature of the value + $value = static::hmac($value) . '+' . $value; + + // store that thing in the cookie global + $_COOKIE[$key] = $value; + + // store the cookie + return setcookie($key, $value, static::lifetime($lifetime), $path, $domain, $secure, $httpOnly); + } + + /** + * 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; + } + } + + /** + * 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 + { + $options['lifetime'] = 253402214400; // 9999-12-31 + return static::set($key, $value, $options); + } + + /** + * 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); + } + + /** + * Checks if a cookie exists + * + * @param string $key + * @return bool + */ + public static function exists(string $key): bool + { + return static::get($key) !== 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); + } + + /** + * 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; + } + + // extract hash and value + $hash = Str::before($string, '+'); + $value = Str::after($string, '+'); + + // 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; + } + + // 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; + } +} diff --git a/kirby/src/Http/Exceptions/NextRouteException.php b/kirby/src/Http/Exceptions/NextRouteException.php new file mode 100755 index 0000000..de85efa --- /dev/null +++ b/kirby/src/Http/Exceptions/NextRouteException.php @@ -0,0 +1,16 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class NextRouteException extends \Exception +{ +} diff --git a/kirby/src/Http/Header.php b/kirby/src/Http/Header.php new file mode 100755 index 0000000..46f9138 --- /dev/null +++ b/kirby/src/Http/Header.php @@ -0,0 +1,316 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Header +{ + // configuration + public static $codes = [ + + // 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', + + // 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' + ]; + + /** + * 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; + + if (empty($charset) === false) { + $header .= '; charset=' . $charset; + } + + if ($send === false) { + return $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 = []; + + foreach ($key as $k => $v) { + $headers[] = static::create($k, $v); + } + + return implode("\r\n", $headers); + } + + // 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); + } + + /** + * 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'; + + // 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; + + if ($send === false) { + return $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 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 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 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 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 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); + + if ($send !== true) { + return $status . "\r\n" . $location; + } + + 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() + ]; + + $options = array_merge($defaults, $params); + + header('Pragma: public'); + header('Expires: 0'); + 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']); + + if ($options['size']) { + header('Content-Length: ' . $options['size']); + } + + header('Connection: close'); + } +} diff --git a/kirby/src/Http/Idn.php b/kirby/src/Http/Idn.php new file mode 100755 index 0000000..4c5dd2e --- /dev/null +++ b/kirby/src/Http/Idn.php @@ -0,0 +1,27 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Idn +{ + public static function decode(string $domain) + { + return (new Punycode())->decode($domain); + } + + public static function encode(string $domain) + { + return (new Punycode())->encode($domain); + } +} diff --git a/kirby/src/Http/Params.php b/kirby/src/Http/Params.php new file mode 100755 index 0000000..5d0fa44 --- /dev/null +++ b/kirby/src/Http/Params.php @@ -0,0 +1,149 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Params extends Query +{ + /** + * @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']; + } + + 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 + ]; + } + + $slash = false; + + if (is_string($path) === true) { + $slash = substr($path, -1, 1) === '/'; + $path = Str::split($path, '/'); + } + + if (is_array($path) === true) { + $params = []; + $separator = static::separator(); + + foreach ($path as $index => $p) { + if (strpos($p, $separator) === false) { + continue; + } + + $paramParts = Str::split($p, $separator); + $paramKey = $paramParts[0]; + $paramValue = $paramParts[1] ?? null; + + $params[$paramKey] = $paramValue; + unset($path[$index]); + } + + return [ + 'path' => $path, + 'params' => $params, + 'slash' => $slash + ]; + } + + 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; + } + + 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 + */ + public function toString($leadingSlash = false, $trailingSlash = false): string + { + if ($this->isEmpty() === true) { + return ''; + } + + $params = []; + $separator = static::separator(); + + foreach ($this as $key => $value) { + if ($value !== null && $value !== '') { + $params[] = $key . $separator . $value; + } + } + + if (empty($params) === true) { + return ''; + } + + $params = implode('/', $params); + + $leadingSlash = $leadingSlash === true ? '/' : null; + $trailingSlash = $trailingSlash === true ? '/' : null; + + return $leadingSlash . $params . $trailingSlash; + } +} diff --git a/kirby/src/Http/Path.php b/kirby/src/Http/Path.php new file mode 100755 index 0000000..eaa014a --- /dev/null +++ b/kirby/src/Http/Path.php @@ -0,0 +1,47 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Path extends Collection +{ + public function __construct($items) + { + if (is_string($items) === true) { + $items = Str::split($items, '/'); + } + + parent::__construct($items ?? []); + } + + public function __toString(): string + { + return $this->toString(); + } + + public function toString(bool $leadingSlash = false, bool $trailingSlash = false): string + { + if (empty($this->data) === true) { + return ''; + } + + $path = implode('/', $this->data); + + $leadingSlash = $leadingSlash === true ? '/' : null; + $trailingSlash = $trailingSlash === true ? '/' : null; + + return $leadingSlash . $path . $trailingSlash; + } +} diff --git a/kirby/src/Http/Query.php b/kirby/src/Http/Query.php new file mode 100755 index 0000000..0956802 --- /dev/null +++ b/kirby/src/Http/Query.php @@ -0,0 +1,58 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Query extends Obj +{ + public function __construct($query) + { + if (is_string($query) === true) { + parse_str(ltrim($query, '?'), $query); + } + + parent::__construct($query ?? []); + } + + public function isEmpty(): bool + { + return empty((array)$this) === true; + } + + public function isNotEmpty(): bool + { + return empty((array)$this) === false; + } + + public function __toString(): string + { + return $this->toString(); + } + + public function toString($questionMark = false): string + { + $query = http_build_query($this); + + if (empty($query) === true) { + return ''; + } + + if ($questionMark === true) { + $query = '?' . $query; + } + + return $query; + } +} diff --git a/kirby/src/Http/Remote.php b/kirby/src/Http/Remote.php new file mode 100755 index 0000000..c5ab761 --- /dev/null +++ b/kirby/src/Http/Remote.php @@ -0,0 +1,367 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Remote +{ + /** + * @var array + */ + public static $defaults = [ + 'agent' => null, + 'basicAuth' => null, + 'body' => true, + 'data' => [], + 'encoding' => 'utf-8', + 'file' => null, + 'headers' => [], + 'method' => 'GET', + 'progress' => null, + 'test' => false, + 'timeout' => 10, + ]; + + /** + * @var string + */ + public $content; + + /** + * @var resource + */ + public $curl; + + /** + * @var array + */ + public $curlopt = []; + + /** + * @var int + */ + public $errorCode; + + /** + * @var string + */ + public $errorMessage; + + /** + * @var array + */ + public $headers = []; + + /** + * @var array + */ + public $info = []; + + /** + * @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; + } + + /** + * Constructor + * + * @param string $url + * @param array $options + */ + public function __construct(string $url, array $options = []) + { + // set all options + $this->options = array_merge(static::$defaults, $options); + + // add the url + $this->options['url'] = $url; + + // 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] ?? [])); + } + + /** + * 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; + } + + /** + * Sets up all curl options and sends the request + * + * @return self + */ + 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_SSL_VERIFYPEER => false, + 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); + } + + return strlen($header); + } + ]; + + // 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; + } + } + + $this->curlopt[CURLOPT_HTTPHEADER] = $headers; + } + + // 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']; + } + + // 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; + } + + if ($this->options['test'] === true) { + return $this; + } + + // start a curl request + $this->curl = curl_init(); + + 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); + + if ($this->errorCode) { + throw new Exception($this->errorMessage, $this->errorCode); + } + + curl_close($this->curl); + + return $this; + } + + /** + * Static method to send a GET request + * + * @param string $url + * @param array $params + * @return self + */ + public static function get(string $url, array $params = []) + { + $defaults = [ + 'method' => 'GET', + '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; + } + + // remove the data array from the options + unset($options['data']); + + return new static($url, $options); + } + + /** + * 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; + } + + /** + * 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 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; + } + } + + /** + * Static method to init this class and send a request + * + * @param string $url + * @param array $params + * @return self + */ + 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']; + } +} diff --git a/kirby/src/Http/Request.php b/kirby/src/Http/Request.php new file mode 100755 index 0000000..c83645a --- /dev/null +++ b/kirby/src/Http/Request.php @@ -0,0 +1,380 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Request +{ + /** + * The auth object if available + * + * @var BearerAuth|BasicAuth|false|null + */ + protected $auth; + + /** + * 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 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 Method type + * + * @var string + */ + protected $method; + + /** + * All options that have been passed to + * the request in the constructor + * + * @var array + */ + protected $options; + + /** + * 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; + + /** + * Request URL object + * + * @var Uri + */ + protected $url; + + /** + * 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 = $options['method'] ?? $_SERVER['REQUEST_METHOD'] ?? 'GET'; + + if (isset($options['body']) === true) { + $this->body = new Body($options['body']); + } + + if (isset($options['files']) === true) { + $this->files = new Files($options['files']); + } + + if (isset($options['query']) === true) { + $this->query = new Query($options['query']); + } + + if (isset($options['url']) === true) { + $this->url = new Uri($options['url']); + } + } + + /** + * 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() + ]; + } + + /** + * 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; + } + + if ($auth = $this->options['auth'] ?? $this->header('authorization')) { + $type = Str::before($auth, ' '); + $token = Str::after($auth, ' '); + $class = 'Kirby\\Http\\Request\\Auth\\' . ucfirst($type) . 'Auth'; + + if (class_exists($class) === false) { + return $this->auth = false; + } + + return $this->auth = new $class($token); + } + + return $this->auth = false; + } + + /** + * Returns the Body object + * + * @return \Kirby\Http\Request\Body + */ + public function body() + { + return $this->body = $this->body ?? new Body(); + } + + /** + * Checks if the request has been made from the command line + * + * @return bool + */ + public function cli(): bool + { + return Server::cli(); + } + + /** + * 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'); + } + + /** + * Returns the request input as array + * + * @return array + */ + public function data(): array + { + return array_merge($this->body()->toArray(), $this->query()->toArray()); + } + + /** + * Returns the domain + * + * @return string + */ + public function domain(): string + { + return $this->url()->domain(); + } + + /** + * 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); + } + + /** + * Returns the Files object + * + * @return \Kirby\Cms\Files + */ + public function files() + { + return $this->files = $this->files ?? new Files(); + } + + /** + * 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); + } + + /** + * 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; + } + + /** + * Return all headers with polyfill for + * missing getallheaders function + * + * @return array + */ + public function headers(): array + { + $headers = []; + + foreach ($_SERVER as $key => $value) { + if (substr($key, 0, 5) !== 'HTTP_' && substr($key, 0, 14) !== 'REDIRECT_HTTP_') { + continue; + } + + // remove HTTP_ + $key = str_replace(['REDIRECT_HTTP_', 'HTTP_'], '', $key); + + // convert to lowercase + $key = strtolower($key); + + // replace _ with spaces + $key = str_replace('_', ' ', $key); + + // uppercase first char in each word + $key = ucwords($key); + + // convert spaces to dashes + $key = str_replace(' ', '-', $key); + + $headers[$key] = $value; + } + + return $headers; + } + + /** + * 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); + } + + /** + * 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\Query + */ + public function query() + { + return $this->query = $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 = $this->url ?? Uri::current(); + } +} diff --git a/kirby/src/Http/Request/Auth/BasicAuth.php b/kirby/src/Http/Request/Auth/BasicAuth.php new file mode 100755 index 0000000..4df6e8f --- /dev/null +++ b/kirby/src/Http/Request/Auth/BasicAuth.php @@ -0,0 +1,78 @@ +credentials = base64_decode($token); + $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 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 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 new file mode 100755 index 0000000..2c5b1c2 --- /dev/null +++ b/kirby/src/Http/Request/Auth/BearerAuth.php @@ -0,0 +1,54 @@ +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'; + } +} diff --git a/kirby/src/Http/Request/Body.php b/kirby/src/Http/Request/Body.php new file mode 100755 index 0000000..f9b6692 --- /dev/null +++ b/kirby/src/Http/Request/Body.php @@ -0,0 +1,129 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Body +{ + use Data; + + /** + * The raw body content + * + * @var string|array + */ + protected $contents; + + /** + * 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; + } + + /** + * 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; + } + + /** + * 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(); + + // 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); + + 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 (is_array($parsed)) { + return $this->data = $parsed; + } + } + + 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()); + } + + /** + * 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 new file mode 100755 index 0000000..0a6bc7f --- /dev/null +++ b/kirby/src/Http/Request/Data.php @@ -0,0 +1,84 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +trait Data +{ + /** + * 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 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; + } + + /** + * 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()); + } +} diff --git a/kirby/src/Http/Request/Files.php b/kirby/src/Http/Request/Files.php new file mode 100755 index 0000000..d5304dd --- /dev/null +++ b/kirby/src/Http/Request/Files.php @@ -0,0 +1,73 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Files +{ + use Data; + + /** + * 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; + } + + $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; + } + } + } + + /** + * 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 new file mode 100755 index 0000000..946218d --- /dev/null +++ b/kirby/src/Http/Request/Query.php @@ -0,0 +1,78 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Query +{ + use 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; + } + } + + /** + * Returns the Query data as array + * + * @return array + */ + public function data(): array + { + return $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(); + } +} diff --git a/kirby/src/Http/Response.php b/kirby/src/Http/Response.php new file mode 100755 index 0000000..ca7596a --- /dev/null +++ b/kirby/src/Http/Response.php @@ -0,0 +1,308 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Response +{ + /** + * Store for all registered headers, + * which will be sent with the response + * + * @var array + */ + protected $headers = []; + + /** + * The response body + * + * @var string + */ + protected $body; + + /** + * The HTTP response code + * + * @var int + */ + protected $code; + + /** + * The content type for the response + * + * @var string + */ + protected $type; + + /** + * 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; + } + + // 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'; + } + } + + /** + * 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 ''; + } + } + + /** + * 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 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 + * @return self + */ + public static function download(string $file, string $filename = null) + { + if (file_exists($file) === false) { + throw new Exception('The file could not be found'); + } + + $filename = $filename ?? basename($file); + $modified = filemtime($file); + $body = file_get_contents($file); + $size = strlen($body); + + return new static([ + 'body' => $body, + 'type' => 'application/force-download', + 'headers' => [ + 'Pragma' => 'public', + 'Expires' => '0', + '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' + ] + ]); + } + + /** + * Creates a response for a file and + * sends the file content to the browser + * + * @param string $file + * @return self + */ + public static function file(string $file) + { + return new static(F::read($file), F::extensionToMime(F::extension($file))); + } + + /** + * 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; + } + + /** + * 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 self + */ + 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 : null); + } + + return new static([ + 'body' => $body, + 'code' => $code, + 'type' => 'application/json', + 'headers' => $headers + ]); + } + + /** + * Creates a redirect response, + * which will send the visitor to the + * given location. + * + * @param string $location + * @param int $code + * @return self + */ + public static function redirect(?string $location = null, ?int $code = null) + { + return new static([ + 'code' => $code ?? 302, + 'headers' => [ + 'Location' => Url::unIdn($location ?? '/') + ] + ]); + } + + /** + * 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()); + + // send all custom headers + foreach ($this->headers() as $key => $value) { + header($key . ': ' . $value); + } + + // send the content type header + header('Content-Type:' . $this->type() . '; charset=' . $this->charset()); + + // 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 new file mode 100755 index 0000000..eff1891 --- /dev/null +++ b/kirby/src/Http/Route.php @@ -0,0 +1,230 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Route +{ + /** + * The callback action function + * + * @var Closure + */ + protected $action; + + /** + * Listed of parsed arguments + * + * @var array + */ + protected $arguments = []; + + /** + * An array of all passed attributes + * + * @var array + */ + protected $attributes = []; + + /** + * The registered request method + * + * @var string + */ + protected $method; + + /** + * 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?)' => '(?:/(.*)', + ], + ]; + + /** + * 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 = 'GET', 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; + } + + /** + * 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 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; + } + + /** + * Throws a specific exception to tell + * the router to jump to the next route + * @since 3.0.3 + * + * @return void + */ + public function next(): void + { + throw new Exceptions\NextRouteException('next'); + } + + /** + * 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']); + + // 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); + } + + 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 = []; + } + + // 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); + } + + return false; + } +} diff --git a/kirby/src/Http/Router.php b/kirby/src/Http/Router.php new file mode 100755 index 0000000..90903f4 --- /dev/null +++ b/kirby/src/Http/Router.php @@ -0,0 +1,168 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Router +{ + public static $beforeEach; + public static $afterEach; + + /** + * Store for the current route, + * if one can be found + * + * @var Route|null + */ + protected $route; + + /** + * 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' => [], + ]; + + /** + * 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'); + } + + $methods = array_map('trim', explode('|', strtoupper($props['method'] ?? 'GET'))); + $patterns = is_array($props['pattern']) === false ? [$props['pattern']] : $props['pattern']; + + if ($methods === ['ALL']) { + $methods = array_keys($this->routes); + } + + foreach ($methods as $method) { + foreach ($patterns as $pattern) { + $this->routes[$method][] = new Route($pattern, $method, $props['action'], $props); + } + } + } + } + + /** + * 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 = $path ?? ''; + $ignore = []; + $result = null; + $loop = true; + + while ($loop === true) { + $route = $this->find($path, $method, $ignore); + + if (is_a(static::$beforeEach, 'Closure') === true) { + (static::$beforeEach)($route, $path, $method); + } + + try { + if ($callback) { + $result = $callback($route); + } else { + $result = $route->action()->call($route, ...$route->arguments()); + } + + $loop = false; + } catch (Exceptions\NextRouteException $e) { + $ignore[] = $route; + } + + if (is_a(static::$afterEach, 'Closure') === true) { + $result = (static::$afterEach)($route, $path, $method, $result); + } + } + + return $result; + } + + /** + * 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); + } + + // remove leading and trailing slashes + $path = trim($path, '/'); + + 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 new file mode 100755 index 0000000..1ccf919 --- /dev/null +++ b/kirby/src/Http/Server.php @@ -0,0 +1,169 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Server +{ + /** + * Cache for the cli status + * + * @var bool|null + */ + public static $cli; + + /** + * 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); + } + + /** + * 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 = strip_tags($value); + $value = preg_replace('![^\w.:-]+!iu', '', $value); + $value = trim($value, '-'); + $value = htmlspecialchars($value); + break; + case 'SERVER_PORT': + case 'HTTP_X_FORWARDED_PORT': + $value = (int)(preg_replace('![^0-9]+!', '', $value)); + break; + } + + return $value; + } + + /** + * Returns the correct port number + * + * @param bool $forwarded + * @return int + */ + public static function port(bool $forwarded = false): int + { + $port = $forwarded === true ? static::get('HTTP_X_FORWARDED_PORT') : null; + + if (empty($port) === true) { + $port = static::get('SERVER_PORT'); + } + + return $port; + } + + /** + * Checks for a https request + * + * @return bool + */ + public static function https(): bool + { + if (isset($_SERVER['HTTPS']) && !empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off') { + return true; + } elseif (static::port() === 443) { + return true; + } elseif (in_array(static::get('HTTP_X_FORWARDED_PROTO'), ['https', 'https, http'])) { + return true; + } else { + return false; + } + } + + /** + * Returns the correct host + * + * @param bool $forwarded + * @return string + */ + public static function host(bool $forwarded = false): string + { + $host = $forwarded === true ? static::get('HTTP_X_FORWARDED_HOST') : null; + + if (empty($host) === true) { + $host = static::get('SERVER_NAME'); + } + + if (empty($host) === true) { + $host = static::get('SERVER_ADDR'); + } + + return explode(':', $host)[0]; + } +} diff --git a/kirby/src/Http/Uri.php b/kirby/src/Http/Uri.php new file mode 100755 index 0000000..190ae89 --- /dev/null +++ b/kirby/src/Http/Uri.php @@ -0,0 +1,563 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +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 $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'] = $props['params'] ?? $extract['params']; + $props['path'] = $extract['path']; + $props['slash'] = $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 self + */ + 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 + * @return self + */ + public static function current(array $props = [], bool $forwarded = false) + { + if (static::$current !== null) { + return static::$current; + } + + $uri = Server::get('REQUEST_URI'); + $uri = preg_replace('!^(http|https)\:\/\/' . Server::get('HTTP_HOST') . '!', '', $uri); + $uri = parse_url('http://getkirby.com' . $uri); + + $url = new static(array_merge([ + 'scheme' => Server::https() === true ? 'https' : 'http', + 'host' => Server::host($forwarded), + 'port' => Server::port($forwarded), + 'path' => $uri['path'] ?? null, + 'query' => $uri['query'] ?? null, + ], $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 self + */ + 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 + * @return string + */ + public static function index(array $props = [], bool $forwarded = false) + { + if (Server::cli() === true) { + $path = null; + } else { + $path = Server::get('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, '/'); + } + + if ($path === '.') { + $path = null; + } + + return static::current(array_merge($props, [ + 'path' => $path, + 'query' => null, + 'fragment' => null, + ]), $forwarded); + } + + + /** + * Checks if the host exists + * + * @return bool + */ + public function isAbsolute(): bool + { + return empty($this->host) === false; + } + + /** + * @param string|null $fragment + * @return self + */ + public function setFragment(string $fragment = null) + { + $this->fragment = $fragment ? ltrim($fragment, '#') : null; + return $this; + } + + /** + * @param string $host + * @return self + */ + public function setHost(string $host = null) + { + $this->host = $host; + return $this; + } + + /** + * @param \Kirby\Http\Params|string|array|null $params + * @return self + */ + 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 self + */ + public function setPassword(string $password = null) + { + $this->password = $password; + return $this; + } + + /** + * @param \Kirby\Http\Path|string|array|null $path + * @return self + */ + 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 self + */ + 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 self + */ + public function setQuery($query = null) + { + $this->query = is_a($query, 'Kirby\Http\Query') === true ? $query : new Query($query); + return $this; + } + + /** + * @param string $scheme + * @return self + */ + 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 self + */ + public function setSlash(bool $slash = false) + { + $this->slash = $slash; + return $this; + } + + /** + * @param string|null $username + * @return self + */ + 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 self + */ + public function unIdn() + { + if (empty($this->host) === false) { + $this->setHost(Idn::encode($this->host)); + } + return $this; + } +} diff --git a/kirby/src/Http/Url.php b/kirby/src/Http/Url.php new file mode 100755 index 0000000..da21800 --- /dev/null +++ b/kirby/src/Http/Url.php @@ -0,0 +1,287 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Url +{ + /** + * The base Url to build absolute Urls from + * + * @var string + */ + public static $home = '/'; + + /** + * 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)); + } + + /** + * 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 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 $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 url to the executed script + * + * @param array $props + * @param bool $forwarded + * @return string + */ + public static function index(array $props = [], bool $forwarded = false): string + { + return Uri::index($props, $forwarded)->toString(); + } + + /** + * Checks if an URL is absolute + * + * @param string $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 + return preg_match('!^(//|[a-z0-9+-.]+://|mailto:|tel:)!i', $url) === 1; + } + + /** + * Convert a relative path into an absolute URL + * + * @param string $path + * @param string $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 (static::isAbsolute($path)) { + return $path; + } + + // build the full url + $path = ltrim($path, '/'); + $home = $home ?? static::home(); + + if (empty($path) === true) { + return $home; + } + + return $home === '/' ? '/' . $path : $home . '/' . $path; + } + + /** + * Returns the path for the given url + * + * @param string|array|null $url + * @param bool $leadingSlash + * @param bool $trailingSlash + * @return xtring + */ + 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(); + } + + /** + * Return the last url the user has been on if detectable + * + * @return string + */ + public static function last(): string + { + return $_SERVER['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); + + $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; + + $url = $base ? $uri->base() : $uri->toString(); + $url = str_replace('www.', '', $url); + + 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 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(); + } + + /** + * 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 + { + // keep relative urls + if (substr($path, 0, 2) === './' || substr($path, 0, 3) === '../') { + return $path; + } + + $url = static::makeAbsolute($path); + + if ($options === null) { + return $url; + } + + 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); + } +} diff --git a/kirby/src/Http/Visitor.php b/kirby/src/Http/Visitor.php new file mode 100755 index 0000000..4ae7102 --- /dev/null +++ b/kirby/src/Http/Visitor.php @@ -0,0 +1,252 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Visitor +{ + /** + * IP address + * @var string|null + */ + protected $ip; + + /** + * user agent + * @var string|null + */ + protected $userAgent; + + /** + * accepted language + * @var string|null + */ + protected $acceptedLanguage; + + /** + * 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'] ?? getenv('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'] ?? ''); + } + + /** + * 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; + } + + /** + * 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; + + $languages[$locale] = new Obj([ + 'code' => $code, + 'locale' => $locale, + 'original' => $value, + 'quality' => $language['quality'], + 'region' => $region, + ]); + } + + 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'; + + foreach ($this->acceptedLanguages() as $language) { + if ($language->$mode() === $code) { + return true; + } + } + + 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(); + } + + $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 = []; + + foreach ($accepted as $mime) { + $mimes[$mime['value']] = new Obj([ + 'type' => $mime['value'], + 'quality' => $mime['quality'], + ]); + } + + 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); + } + + /** + * 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; + } + } + } + + 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'; + } + + /** + * 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; + } +} diff --git a/kirby/src/Image/Camera.php b/kirby/src/Image/Camera.php new file mode 100755 index 0000000..a59e4d9 --- /dev/null +++ b/kirby/src/Image/Camera.php @@ -0,0 +1,93 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Camera +{ + /** + * Make exif data + * + * @var string|null + */ + protected $make; + + /** + * Model exif data + * + * @var string|null + */ + protected $model; + + /** + * Constructor + * + * @param array $exif + */ + public function __construct(array $exif) + { + $this->make = @$exif['Make']; + $this->model = @$exif['Model']; + } + + /** + * 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; + } + + /** + * 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); + } + + /** + * 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 new file mode 100755 index 0000000..0c4cafb --- /dev/null +++ b/kirby/src/Image/Darkroom.php @@ -0,0 +1,103 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Darkroom +{ + public static $types = [ + 'gd' => 'Kirby\Image\Darkroom\GdLib', + 'im' => 'Kirby\Image\Darkroom\ImageMagick' + ]; + + protected $settings = []; + + public function __construct(array $settings = []) + { + $this->settings = array_merge($this->defaults(), $settings); + } + + 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); + } + + protected function defaults(): array + { + return [ + 'autoOrient' => true, + 'crop' => false, + 'blur' => false, + 'grayscale' => false, + 'height' => null, + 'quality' => 90, + 'width' => null, + ]; + } + + 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 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 bw option + if (isset($options['bw']) === true) { + $options['grayscale'] = $options['bw']; + unset($options['bw']); + } + + if ($options['quality'] === null) { + $options['quality'] = $this->settings['quality']; + } + + return $options; + } + + public function preprocess(string $file, array $options = []) + { + $options = $this->options($options); + $image = new Image($file); + $dimensions = $image->dimensions()->thumb($options); + + $options['width'] = $dimensions->width(); + $options['height'] = $dimensions->height(); + + return $options; + } + + 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 new file mode 100755 index 0000000..3d9b08c --- /dev/null +++ b/kirby/src/Image/Darkroom/GdLib.php @@ -0,0 +1,73 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class GdLib extends Darkroom +{ + public function process(string $file, array $options = []): array + { + $options = $this->preprocess($file, $options); + + $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->toFile($file, null, $options['quality']); + + return $options; + } + + protected function autoOrient(SimpleImage $image, $options) + { + if ($options['autoOrient'] === false) { + return $image; + } + + return $image->autoOrient(); + } + + 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']); + } + + protected function blur(SimpleImage $image, array $options) + { + if ($options['blur'] === false) { + return $image; + } + + return $image->blur('gaussian', (int)$options['blur']); + } + + protected function grayscale(SimpleImage $image, array $options) + { + if ($options['grayscale'] === false) { + return $image; + } + + return $image->desaturate(); + } +} diff --git a/kirby/src/Image/Darkroom/ImageMagick.php b/kirby/src/Image/Darkroom/ImageMagick.php new file mode 100755 index 0000000..b997552 --- /dev/null +++ b/kirby/src/Image/Darkroom/ImageMagick.php @@ -0,0 +1,140 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class ImageMagick extends Darkroom +{ + protected function autoOrient(string $file, array $options) + { + if ($options['autoOrient'] === true) { + return '-auto-orient'; + } + } + + protected function blur(string $file, array $options) + { + if ($options['blur'] !== false) { + return '-blur 0x' . $options['blur']; + } + } + + protected function coalesce(string $file, array $options) + { + if (F::extension($file) === 'gif') { + return '-coalesce'; + } + } + + protected function convert(string $file, array $options): string + { + return sprintf($options['bin'] . ' "%s"', $file); + } + + protected function defaults(): array + { + return parent::defaults() + [ + 'bin' => 'convert', + 'interlace' => false, + ]; + } + + protected function grayscale(string $file, array $options) + { + if ($options['grayscale'] === true) { + return '-colorspace gray'; + } + } + + protected function interlace(string $file, array $options) + { + if ($options['interlace'] === true) { + return '-interlace line'; + } + } + + 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); + + // remove all null values and join the parts + $command = implode(' ', array_filter($command)); + + // 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); + } + + return $options; + } + + protected function quality(string $file, array $options): string + { + return '-quality ' . $options['quality']; + } + + protected function resize(string $file, array $options): string + { + // simple resize + if ($options['crop'] === false) { + return sprintf('-resize %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' + ]; + + // translate the gravity option into something imagemagick understands + $gravity = $gravities[$options['crop']] ?? 'Center'; + + $command = sprintf('-resize %sx%s^', $options['width'], $options['height']); + $command .= sprintf(' -gravity %s -crop %sx%s+0+0', $gravity, $options['width'], $options['height']); + + return $command; + } + + protected function save(string $file, array $options): string + { + return sprintf('-limit thread 1 "%s"', $file); + } + + protected function strip(string $file, array $options): string + { + return '-strip'; + } +} diff --git a/kirby/src/Image/Dimensions.php b/kirby/src/Image/Dimensions.php new file mode 100755 index 0000000..ed20b88 --- /dev/null +++ b/kirby/src/Image/Dimensions.php @@ -0,0 +1,430 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Dimensions +{ + /** + * the height of the parent object + * + * @var int + */ + public $height = 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; + } + + /** + * 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; + } + + /** + * Crops the dimensions by width and height + * + * @param int $width + * @param int $height + * @return self + */ + public function crop(int $width, int $height = null) + { + $this->width = $width; + $this->height = $width; + + if ($height !== 0 && $height !== null) { + $this->height = $height; + } + + return $this; + } + + /** + * 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 self 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(); + + 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; + } + + /** + * 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 $fit the max height + * @param bool $force If true, the dimensions will be + * upscaled to fit the box if smaller + * @return self 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 $fit the max width + * @param bool $force If true, the dimensions will be + * upscaled to fit the box if smaller + * @return self 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; + } + + $ratio = $this->ratio(); + $mode = $ref === 'width'; + $this->width = $mode ? $fit : (int)round($fit * $ratio); + $this->height = !$mode ? $fit : (int)round($fit / $ratio); + + 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 $fit the max width + * @param bool $force If true, the dimensions will be + * upscaled to fit the box if smaller + * @return self 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 $width the max height + * @param int $height the max width + * @param bool $force + * @return self + */ + 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 width + if ($this->width > $width) { + $this->fitWidth($width); + } + } + + return $this; + } + + /** + * Detect the dimensions for an image file + * + * @param string $root + * @return self + */ + 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); + } + + /** + * Detect the dimensions for a svg file + * + * @param string $root + * @return self + */ + 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); + + if ($xml !== false) { + $attr = $xml->attributes(); + $width = (float)($attr->width); + $height = (float)($attr->height); + if (($width === 0.0 || $height === 0.0) && empty($attr->viewBox) === false) { + $box = explode(' ', $attr->viewBox); + $width = (float)($box[2] ?? 0); + $height = (float)($box[3] ?? 0); + } + } + + return new static($width, $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; + } + + if ($this->portrait()) { + return 'portrait'; + } + + if ($this->landscape()) { + return 'landscape'; + } + + return 'square'; + } + + /** + * 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; + } + + return 0; + } + + /** + * @param int $width + * @param int $height + * @param bool $force + * @return self + */ + 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; + } + + /** + * Resize and crop + * + * @param array $options + * @return self + */ + 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; + } + + 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(), + ]; + } + + /** + * 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 new file mode 100755 index 0000000..08aeef8 --- /dev/null +++ b/kirby/src/Image/Exif.php @@ -0,0 +1,296 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Exif +{ + /** + * the parent image object + * @var Image + */ + protected $image; + + /** + * the raw exif array + * @var array + */ + protected $data = []; + + /** + * the camera object with model and make + * @var Camera + */ + protected $camera; + + /** + * the location object + * @var Location + */ + protected $location; + + /** + * the timestamp + * + * @var string + */ + protected $timestamp; + + /** + * the exposure value + * + * @var string + */ + protected $exposure; + + /** + * the aperture value + * + * @var string + */ + protected $aperture; + + /** + * iso value + * + * @var string + */ + protected $iso; + + /** + * focal length + * + * @var string + */ + protected $focalLength; + + /** + * 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(); + } + + /** + * 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; + } + + 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; + } + + return $this->location = new Location($this->data); + } + + /** + * 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 aperture + * + * @return string|null + */ + public function aperture() + { + return $this->aperture; + } + + /** + * 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 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; + } + + /** + * Read the exif data of the image object if possible + * + * @return mixed + */ + protected function read(): array + { + if (function_exists('exif_read_data') === false) { + return []; + } + + $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'] ?? []; + } + + /** + * 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 $this->data['FileDateTime'] ?? $this->image->modified(); + } + + /** + * Teturn 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() + ]; + } + + /** + * 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 new file mode 100755 index 0000000..def07dc --- /dev/null +++ b/kirby/src/Image/Image.php @@ -0,0 +1,310 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Image extends File +{ + /** + * optional url where the file is reachable + * @var string + */ + protected $url; + + /** + * @var Exif|null + */ + protected $exif; + + /** + * @var Dimensions|null + */ + protected $dimensions; + + /** + * Constructor + * + * @param string $root + * @param string|null $url + */ + public function __construct(string $root = null, string $url = null) + { + parent::__construct($root); + $this->url = $url; + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return array_merge($this->toArray(), [ + 'dimensions' => $this->dimensions(), + 'exif' => $this->exif(), + ]); + } + + /** + * Returns a full link to this file + * Perfect for debugging in connection with echo + * + * @return string + */ + public function __toString(): string + { + return $this->root; + } + + /** + * 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 ($this->extension() === 'svg') { + return $this->dimensions = Dimensions::forSvg($this->root); + } + + return $this->dimensions = new Dimensions(0, 0); + } + + /* + * 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()); + } + + /** + * Returns the exif object for this file (if image) + * + * @return \Kirby\Image\Exif + */ + public function exif() + { + if ($this->exif !== null) { + return $this->exif; + } + $this->exif = new Exif($this); + return $this->exif; + } + + /** + * Sends an appropriate header for the asset + * + * @param bool $send + * @return \Kirby\Http\Response|string + */ + public function header(bool $send = true) + { + $response = new Response(); + $response->type($this->mime()); + return $send === true ? $response->send() : $response; + } + + /** + * Returns the height of the asset + * + * @return int + */ + public function height(): int + { + return $this->dimensions()->height(); + } + + /** + * @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); + } + + /** + * 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 square + * + * @return bool + */ + public function isSquare(): bool + { + return $this->dimensions()->square(); + } + + /** + * Runs a set of validations on the image object + * + * @param array $rules + * @return bool + */ + public function match(array $rules): bool + { + if (($rules['mime'] ?? null) !== null) { + if (Mime::isAccepted($this->mime(), $rules['mime']) !== true) { + throw new Exception(I18n::template('error.file.mime.invalid', [ + 'mime' => $this->mime() + ])); + } + } + + $rules = array_change_key_case($rules); + + $validations = [ + 'maxsize' => ['size', 'max'], + 'minsize' => ['size', 'min'], + 'maxwidth' => ['width', 'max'], + 'minwidth' => ['width', 'min'], + 'maxheight' => ['height', 'max'], + 'minheight' => ['height', 'min'], + 'orientation' => ['orientation', 'same'] + ]; + + foreach ($validations as $key => $arguments) { + $rule = $rules[$key] ?? null; + + if ($rule !== null) { + $property = $arguments[0]; + $validator = $arguments[1]; + + if (V::$validator($this->$property(), $rule) === false) { + throw new Exception(I18n::template('error.file.' . $key, [ + $property => $rule + ])); + } + } + } + + return true; + } + + /** + * 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(); + } + + /** + * Converts the media object to a + * plain PHP array + * + * @return array + */ + public function toArray(): array + { + return array_merge(parent::toArray(), [ + 'dimensions' => $this->dimensions()->toArray(), + 'exif' => $this->exif()->toArray(), + ]); + } + + /** + * Converts the entire file array into + * a json string + * + * @return string + */ + public function toJson(): string + { + return json_encode($this->toArray()); + } + + /** + * Returns the url + * + * @return string + */ + public function url() + { + return $this->url; + } + + /** + * 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 new file mode 100755 index 0000000..2c4e386 --- /dev/null +++ b/kirby/src/Image/Location.php @@ -0,0 +1,136 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Location +{ + /** + * latitude + * + * @var float|null + */ + protected $lat; + + /** + * 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']); + } + } + + /** + * Returns the latitude + * + * @return float|null + */ + public function lat() + { + return $this->lat; + } + + /** + * 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; + + $hemi = strtoupper($hemi); + $flip = ($hemi == 'W' || $hemi == 'S') ? -1 : 1; + + 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); + + if (count($parts) == 1) { + return $parts[0]; + } + + 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() + ]; + } + + /** + * 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(); + } +} diff --git a/kirby/src/Session/AutoSession.php b/kirby/src/Session/AutoSession.php new file mode 100755 index 0000000..cf9b33a --- /dev/null +++ b/kirby/src/Session/AutoSession.php @@ -0,0 +1,171 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class AutoSession +{ + protected $sessions; + protected $options; + + protected $createdSession; + + /** + * Creates a new AutoSession instance + * + * @param \Kirby\Session\SessionStore|string $store SessionStore object or a path to the storage directory (uses the FileSessionStore) + * @param array $options Optional additional options: + * - `durationNormal`: Duration of normal sessions in seconds; defaults to 2 hours + * - `durationLong`: Duration of "remember me" sessions in seconds; defaults to 2 weeks + * - `timeout`: Activity timeout in seconds (integer or false for none); *only* used for normal sessions; defaults to `1800` (half an hour) + * - `cookieName`: Name to use for the session cookie; defaults to `kirby_session` + * - `gcInterval`: How often should the garbage collector be run?; integer or `false` for never; defaults to `100` + */ + public function __construct($store, array $options = []) + { + // merge options with defaults + $this->options = array_merge([ + 'durationNormal' => 7200, + 'durationLong' => 1209600, + 'timeout' => 1800, + 'cookieName' => 'kirby_session', + 'gcInterval' => 100 + ], $options); + + // create an internal instance of the low-level Sessions class + $this->sessions = new Sessions($store, [ + 'cookieName' => $this->options['cookieName'], + 'gcInterval' => $this->options['gcInterval'] + ]); + } + + /** + * Returns the automatic session + * + * @param array $options Optional additional options: + * - `detect`: Whether to allow sessions in the `Authorization` HTTP header (`true`) or only in the session cookie (`false`); defaults to `false` + * - `createMode`: When creating a new session, should it be set as a cookie or is it going to be transmitted manually to be used in a header?; defaults to `cookie` + * - `long`: Whether the session is a long "remember me" session or a normal session; defaults to `false` + * @return \Kirby\Session\Session + */ + public function get(array $options = []) + { + // merge options with defaults + $options = array_merge([ + 'detect' => false, + 'createMode' => 'cookie', + 'long' => false + ], $options); + + // determine expiry options based on the session type + if ($options['long'] === true) { + $duration = $this->options['durationLong']; + $timeout = false; + } else { + $duration = $this->options['durationNormal']; + $timeout = $this->options['timeout']; + } + + // get the current session + if ($options['detect'] === true) { + $session = $this->sessions->currentDetected(); + } else { + $session = $this->sessions->current(); + } + + // create a new session + if ($session === null) { + $session = $this->createdSession ?? $this->sessions->create([ + 'mode' => $options['createMode'], + 'startTime' => time(), + 'expiryTime' => time() + $duration, + 'timeout' => $timeout, + 'renewable' => true, + ]); + + // cache the newly created session to ensure that we don't create multiple + $this->createdSession = $session; + } + + // update the session configuration if the $options changed + // always use the less strict value for compatibility with features + // that depend on the less strict behavior + if ($duration > $session->duration()) { + // the duration needs to be extended + $session->duration($duration); + } + if ($session->timeout() !== false) { + // a timeout exists + if ($timeout === false) { + // it needs to be completely disabled + $session->timeout(false); + } elseif (is_int($timeout) && $timeout > $session->timeout()) { + // it needs to be extended + $session->timeout($timeout); + } + } + + // if the session has been created and was not yet initialized, + // update the mode to a custom mode + // don't update back to cookie mode because the "special" behavior always wins + if ($session->token() === null && $options['createMode'] !== 'cookie') { + $session->mode($options['createMode']); + } + + return $session; + } + + /** + * Creates a new empty session that is *not* automatically transmitted to the client + * Useful for custom applications like a password reset link + * Does *not* affect the automatic session + * + * @param array $options Optional additional options: + * - `startTime`: Time the session starts being valid (date string or timestamp); defaults to `now` + * - `expiryTime`: Time the session expires (date string or timestamp); defaults to `+ 2 hours` + * - `timeout`: Activity timeout in seconds (integer or false for none); defaults to `1800` (half an hour) + * - `renewable`: Should it be possible to extend the expiry date?; defaults to `true` + * @return \Kirby\Session\Session + */ + public function createManually(array $options = []) + { + // only ever allow manual transmission mode + // to prevent overwriting our "auto" session + $options['mode'] = 'manual'; + + return $this->sessions->create($options); + } + + /** + * Returns the specified Session object + * @since 3.3.1 + * + * @param string $token Session token, either including or without the key + * @return \Kirby\Session\Session + */ + public function getManually(string $token) + { + return $this->sessions->get($token, 'manual'); + } + + /** + * Deletes all expired sessions + * + * If the `gcInterval` is configured, this is done automatically + * when intializing the AutoSession class + * + * @return void + */ + public function collectGarbage() + { + $this->sessions->collectGarbage(); + } +} diff --git a/kirby/src/Session/FileSessionStore.php b/kirby/src/Session/FileSessionStore.php new file mode 100755 index 0000000..aeaee14 --- /dev/null +++ b/kirby/src/Session/FileSessionStore.php @@ -0,0 +1,484 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class FileSessionStore extends SessionStore +{ + protected $path; + + // state of the session files + protected $handles = []; + protected $isLocked = []; + + /** + * Creates a new instance of the file session store + * + * @param string $path Path to the storage directory + */ + public function __construct(string $path) + { + // create the directory if it doesn't already exist + Dir::make($path, true); + + // store the canonicalized path + $this->path = realpath($path); + + // make sure it is usable for storage + if (!is_writable($this->path)) { + throw new Exception([ + 'key' => 'session.filestore.dirNotWritable', + 'data' => ['path' => $this->path], + 'fallback' => 'The session storage directory "' . $path . '" is not writable', + 'translate' => false, + 'httpCode' => 500 + ]); + } + } + + /** + * Creates a new session ID with the given expiry time + * + * Needs to make sure that the session does not already exist + * and needs to reserve it by locking it exclusively. + * + * @param int $expiryTime Timestamp + * @return string Randomly generated session ID (without timestamp) + */ + public function createId(int $expiryTime): string + { + clearstatcache(); + do { + // use helper from the abstract SessionStore class + $id = static::generateId(); + + $name = $this->name($expiryTime, $id); + $path = $this->path($name); + } while (file_exists($path)); + + // reserve the file + touch($path); + $this->lock($expiryTime, $id); + + // ensure that no other thread already wrote to the same file, otherwise try again + // very unlikely scenario! + $contents = $this->get($expiryTime, $id); + if ($contents !== '') { + // @codeCoverageIgnoreStart + $this->unlock($expiryTime, $id); + return $this->createId($expiryTime); + // @codeCoverageIgnoreEnd + } + + return $id; + } + + /** + * Checks if the given session exists + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @return bool true: session exists, + * false: session doesn't exist + */ + public function exists(int $expiryTime, string $id): bool + { + $name = $this->name($expiryTime, $id); + $path = $this->path($name); + + clearstatcache(); + return is_file($path) === true; + } + + /** + * Locks the given session exclusively + * + * Needs to throw an Exception on error. + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @return void + */ + public function lock(int $expiryTime, string $id) + { + $name = $this->name($expiryTime, $id); + $path = $this->path($name); + + // check if the file is already locked + if (isset($this->isLocked[$name])) { + return; + } + + // lock it exclusively + $handle = $this->handle($name); + $result = flock($handle, LOCK_EX); + + // make a note that the file is now locked + if ($result === true) { + $this->isLocked[$name] = true; + } else { + // @codeCoverageIgnoreStart + throw new Exception([ + 'key' => 'session.filestore.unexpectedFilesystemError', + 'fallback' => 'Unexpected file system error', + 'translate' => false, + 'httpCode' => 500 + ]); + // @codeCoverageIgnoreEnd + } + } + + /** + * Removes all locks on the given session + * + * Needs to throw an Exception on error. + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @return void + */ + public function unlock(int $expiryTime, string $id) + { + $name = $this->name($expiryTime, $id); + $path = $this->path($name); + + // check if the file is already unlocked or doesn't exist + if (!isset($this->isLocked[$name])) { + return; + } elseif ($this->exists($expiryTime, $id) === false) { + unset($this->isLocked[$name]); + return; + } + + // remove the exclusive lock + $handle = $this->handle($name); + $result = flock($handle, LOCK_UN); + + // make a note that the file is no longer locked + if ($result === true) { + unset($this->isLocked[$name]); + } else { + // @codeCoverageIgnoreStart + throw new Exception([ + 'key' => 'session.filestore.unexpectedFilesystemError', + 'fallback' => 'Unexpected file system error', + 'translate' => false, + 'httpCode' => 500 + ]); + // @codeCoverageIgnoreEnd + } + } + + /** + * Returns the stored session data of the given session + * + * Needs to throw an Exception on error. + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @return string + */ + public function get(int $expiryTime, string $id): string + { + $name = $this->name($expiryTime, $id); + $path = $this->path($name); + $handle = $this->handle($name); + + // set read lock to prevent other threads from corrupting the data while we read it + // only if we don't already have a write lock, which is even better + if (!isset($this->isLocked[$name])) { + $result = flock($handle, LOCK_SH); + + if ($result !== true) { + // @codeCoverageIgnoreStart + throw new Exception([ + 'key' => 'session.filestore.unexpectedFilesystemError', + 'fallback' => 'Unexpected file system error', + 'translate' => false, + 'httpCode' => 500 + ]); + // @codeCoverageIgnoreEnd + } + } + + clearstatcache(); + $filesize = filesize($path); + if ($filesize > 0) { + // always read the whole file + rewind($handle); + $string = fread($handle, $filesize); + } else { + // we don't need to read empty files + $string = ''; + } + + // remove the shared lock if we set one above + if (!isset($this->isLocked[$name])) { + $result = flock($handle, LOCK_UN); + + if ($result !== true) { + // @codeCoverageIgnoreStart + throw new Exception([ + 'key' => 'session.filestore.unexpectedFilesystemError', + 'fallback' => 'Unexpected file system error', + 'translate' => false, + 'httpCode' => 500 + ]); + // @codeCoverageIgnoreEnd + } + } + + return $string; + } + + /** + * Stores data to the given session + * + * Needs to make sure that the session exists. + * Needs to throw an Exception on error. + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @param string $data Session data to write + * @return void + */ + public function set(int $expiryTime, string $id, string $data) + { + $name = $this->name($expiryTime, $id); + $path = $this->path($name); + $handle = $this->handle($name); + + // validate that we have an exclusive lock already + if (!isset($this->isLocked[$name])) { + throw new LogicException([ + 'key' => 'session.filestore.notLocked', + 'data' => ['name' => $name], + 'fallback' => 'Cannot write to session "' . $name . '", because it is not locked', + 'translate' => false, + 'httpCode' => 500 + ]); + } + + // delete all file contents first + if (rewind($handle) !== true || ftruncate($handle, 0) !== true) { + // @codeCoverageIgnoreStart + throw new Exception([ + 'key' => 'session.filestore.unexpectedFilesystemError', + 'fallback' => 'Unexpected file system error', + 'translate' => false, + 'httpCode' => 500 + ]); + // @codeCoverageIgnoreEnd + } + + // write the new contents + $result = fwrite($handle, $data); + if (!is_int($result) || $result === 0) { + // @codeCoverageIgnoreStart + throw new Exception([ + 'key' => 'session.filestore.unexpectedFilesystemError', + 'fallback' => 'Unexpected file system error', + 'translate' => false, + 'httpCode' => 500 + ]); + // @codeCoverageIgnoreEnd + } + } + + /** + * Deletes the given session + * + * Needs to throw an Exception on error. + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @return void + */ + public function destroy(int $expiryTime, string $id) + { + $name = $this->name($expiryTime, $id); + $path = $this->path($name); + + // close the file, otherwise we can't delete it on Windows; + // deletion is *not* thread-safe because of this, but + // resurrection of the file is prevented in $this->set() because of + // the check in $this->handle() every time any method is called + $this->unlock($expiryTime, $id); + $this->closeHandle($name); + + // we don't need to delete files that don't exist anymore + if ($this->exists($expiryTime, $id) === false) { + return; + } + + // file still exists, delete it + if (@unlink($path) !== true) { + // @codeCoverageIgnoreStart + throw new Exception([ + 'key' => 'session.filestore.unexpectedFilesystemError', + 'fallback' => 'Unexpected file system error', + 'translate' => false, + 'httpCode' => 500 + ]); + // @codeCoverageIgnoreEnd + } + } + + /** + * Deletes all expired sessions + * + * Needs to throw an Exception on error. + * + * @return void + */ + public function collectGarbage() + { + $iterator = new FilesystemIterator($this->path); + + $currentTime = time(); + foreach ($iterator as $file) { + // make sure that the file is a session file + // prevents deleting files like .gitignore or other unrelated files + if (preg_match('/^[0-9]+\.[a-z0-9]+\.sess$/', $file->getFilename()) !== 1) { + continue; + } + + // extract the data from the filename + $name = $file->getBasename('.sess'); + $expiryTime = (int)Str::before($name, '.'); + $id = Str::after($name, '.'); + + if ($expiryTime < $currentTime) { + // the session has expired, delete it + $this->destroy($expiryTime, $id); + } + } + } + + /** + * Cleans up the open locks and file handles + * + * @codeCoverageIgnore + */ + public function __destruct() + { + // unlock all locked files + foreach ($this->isLocked as $name => $locked) { + $expiryTime = (int)Str::before($name, '.'); + $id = Str::after($name, '.'); + + $this->unlock($expiryTime, $id); + } + + // close all file handles + foreach ($this->handles as $name => $handle) { + $this->closeHandle($name); + } + } + + /** + * Returns the combined name based on expiry time and ID + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @return string + */ + protected function name(int $expiryTime, string $id): string + { + return $expiryTime . '.' . $id; + } + + /** + * Returns the full path to the session file + * + * @param string $name Combined name + * @return string + */ + protected function path(string $name): string + { + return $this->path . '/' . $name . '.sess'; + } + + /** + * Returns a PHP file handle for a session + * + * @param string $name Combined name + * @return resource File handle + */ + protected function handle(string $name) + { + // always verify that the file still exists, even if we already have a handle; + // ensures thread-safeness for recently deleted sessions, see $this->destroy() + $path = $this->path($name); + clearstatcache(); + if (!is_file($path)) { + throw new NotFoundException([ + 'key' => 'session.filestore.notFound', + 'data' => ['name' => $name], + 'fallback' => 'Session file "' . $name . '" does not exist', + 'translate' => false, + 'httpCode' => 404 + ]); + } + + // return from cache + if (isset($this->handles[$name])) { + return $this->handles[$name]; + } + + // open a new handle + $handle = @fopen($path, 'r+b'); + if (!is_resource($handle)) { + throw new Exception([ + 'key' => 'session.filestore.notOpened', + 'data' => ['name' => $name], + 'fallback' => 'Session file "' . $name . '" could not be opened', + 'translate' => false, + 'httpCode' => 500 + ]); + } + + return $this->handles[$name] = $handle; + } + + /** + * Closes an open file handle + * + * @param string $name Combined name + * @return void + */ + protected function closeHandle(string $name) + { + if (!isset($this->handles[$name])) { + return; + } + $handle = $this->handles[$name]; + + unset($this->handles[$name]); + $result = fclose($handle); + + if ($result !== true) { + // @codeCoverageIgnoreStart + throw new Exception([ + 'key' => 'session.filestore.unexpectedFilesystemError', + 'fallback' => 'Unexpected file system error', + 'translate' => false, + 'httpCode' => 500 + ]); + // @codeCoverageIgnoreEnd + } + } +} diff --git a/kirby/src/Session/Session.php b/kirby/src/Session/Session.php new file mode 100755 index 0000000..3bdb0e8 --- /dev/null +++ b/kirby/src/Session/Session.php @@ -0,0 +1,766 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Session +{ + // parent data + protected $sessions; + protected $mode; + + // parts of the token + protected $tokenExpiry; + protected $tokenId; + protected $tokenKey; + + // persistent data + protected $startTime; + protected $expiryTime; + protected $duration; + protected $timeout; + protected $lastActivity; + protected $renewable; + protected $data; + protected $newSession; + + // temporary state flags + protected $updatingLastActivity = false; + protected $destroyed = false; + protected $writeMode = false; + protected $needsRetransmission = false; + + /** + * Creates a new Session instance + * + * @param \Kirby\Session\Sessions $sessions Parent sessions object + * @param string|null $token Session token or null for a new session + * @param array $options Optional additional options: + * - `mode`: Token transmission mode (cookie or manual); defaults to `cookie` + * - `startTime`: Time the session starts being valid (date string or timestamp); defaults to `now` + * - `expiryTime`: Time the session expires (date string or timestamp); defaults to `+ 2 hours` + * - `timeout`: Activity timeout in seconds (integer or false for none); defaults to `1800` (half an hour) + * - `renewable`: Should it be possible to extend the expiry date?; defaults to `true` + */ + public function __construct(Sessions $sessions, $token, array $options) + { + $this->sessions = $sessions; + $this->mode = $options['mode'] ?? 'cookie'; + + if (is_string($token)) { + // existing session + + // set the token as instance vars + $this->parseToken($token); + + // initialize, but only try to write to the session if not read-only + // (only the case for moved sessions) + $this->init(); + if ($this->tokenKey !== null) { + $this->autoRenew(); + } + } elseif ($token === null) { + // new session + + // set data based on options + $this->startTime = static::timeToTimestamp($options['startTime'] ?? time()); + $this->expiryTime = static::timeToTimestamp($options['expiryTime'] ?? '+ 2 hours', $this->startTime); + $this->duration = $this->expiryTime - $this->startTime; + $this->timeout = $options['timeout'] ?? 1800; + $this->renewable = $options['renewable'] ?? true; + $this->data = new SessionData($this, []); + + // validate persistent data + if (time() > $this->expiryTime) { + // session must not already be expired, but the start time may be in the future + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::__construct', 'argument' => '$options[\'expiryTime\']'], + 'translate' => false + ]); + } + if ($this->duration < 0) { + // expiry time must be after start time + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::__construct', 'argument' => '$options[\'startTime\' & \'expiryTime\']'], + 'translate' => false + ]); + } + if (!is_int($this->timeout) && $this->timeout !== false) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::__construct', 'argument' => '$options[\'timeout\']'], + 'translate' => false + ]); + } + if (!is_bool($this->renewable)) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::__construct', 'argument' => '$options[\'renewable\']'], + 'translate' => false + ]); + } + + // set activity time if a timeout was requested + if (is_int($this->timeout)) { + $this->lastActivity = time(); + } + } else { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::__construct', 'argument' => '$token'], + 'translate' => false + ]); + } + + // ensure that all changes are committed on script termination + register_shutdown_function([$this, 'commit']); + } + + /** + * Gets the session token or null if the session doesn't have a token yet + * + * @return string|null + */ + public function token() + { + if ($this->tokenExpiry !== null) { + if (is_string($this->tokenKey)) { + return $this->tokenExpiry . '.' . $this->tokenId . '.' . $this->tokenKey; + } else { + return $this->tokenExpiry . '.' . $this->tokenId; + } + } else { + return null; + } + } + + /** + * Gets or sets the transmission mode + * Setting only works for new sessions that haven't been transmitted yet + * + * @param string $mode Optional new transmission mode + * @return string Transmission mode + */ + public function mode(string $mode = null) + { + if (is_string($mode)) { + // only allow this if this is a new session, otherwise the change + // might not be applied correctly to the current request + if ($this->token() !== null) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::mode', 'argument' => '$mode'], + 'translate' => false + ]); + } + + $this->mode = $mode; + } + + return $this->mode; + } + + /** + * Gets the session start time + * + * @return int Timestamp + */ + public function startTime(): int + { + return $this->startTime; + } + + /** + * Gets or sets the session expiry time + * Setting the expiry time also updates the duration and regenerates the session token + * + * @param string|int $expiryTime Optional new expiry timestamp or time string to set + * @return int Timestamp + */ + public function expiryTime($expiryTime = null): int + { + if (is_string($expiryTime) || is_int($expiryTime)) { + // convert to a timestamp + $expiryTime = static::timeToTimestamp($expiryTime); + + // verify that the expiry time is not in the past + if ($expiryTime <= time()) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::expiryTime', 'argument' => '$expiryTime'], + 'translate' => false + ]); + } + + $this->prepareForWriting(); + $this->expiryTime = $expiryTime; + $this->duration = $expiryTime - time(); + $this->regenerateTokenIfNotNew(); + } elseif ($expiryTime !== null) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::expiryTime', 'argument' => '$expiryTime'], + 'translate' => false + ]); + } + + return $this->expiryTime; + } + + /** + * Gets or sets the session duration + * Setting the duration also updates the expiry time and regenerates the session token + * + * @param int $duration Optional new duration in seconds to set + * @return int Number of seconds + */ + public function duration(int $duration = null): int + { + if (is_int($duration)) { + // verify that the duration is at least 1 second + if ($duration < 1) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::duration', 'argument' => '$duration'], + 'translate' => false + ]); + } + + $this->prepareForWriting(); + $this->duration = $duration; + $this->expiryTime = time() + $duration; + $this->regenerateTokenIfNotNew(); + } + + return $this->duration; + } + + /** + * Gets or sets the session timeout + * + * @param int|false $timeout Optional new timeout to set or false to disable timeout + * @return int|false Number of seconds or false for "no timeout" + */ + public function timeout($timeout = null) + { + if (is_int($timeout) || $timeout === false) { + // verify that the timeout is at least 1 second + if (is_int($timeout) && $timeout < 1) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::timeout', 'argument' => '$timeout'], + 'translate' => false + ]); + } + + $this->prepareForWriting(); + $this->timeout = $timeout; + + if (is_int($timeout)) { + $this->lastActivity = time(); + } else { + $this->lastActivity = null; + } + } elseif ($timeout !== null) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::timeout', 'argument' => '$timeout'], + 'translate' => false + ]); + } + + return $this->timeout; + } + + /** + * Gets or sets the renewable flag + * Automatically renews the session if renewing gets enabled + * + * @param bool $renewable Optional new renewable flag to set + * @return bool + */ + public function renewable(bool $renewable = null): bool + { + if (is_bool($renewable)) { + $this->prepareForWriting(); + $this->renewable = $renewable; + $this->autoRenew(); + } + + return $this->renewable; + } + + /** + * Returns the session data object + * + * @return \Kirby\Session\SessionData + */ + public function data() + { + return $this->data; + } + + /** + * Magic call method that proxies all calls to session data methods + * + * @param string $name Method name (one of set, increment, decrement, get, pull, remove, clear) + * @param array $arguments Method arguments + * @return mixed + */ + public function __call(string $name, array $arguments) + { + // validate that we can handle the called method + if (!in_array($name, ['set', 'increment', 'decrement', 'get', 'pull', 'remove', 'clear'])) { + throw new BadMethodCallException([ + 'data' => ['method' => 'Session::' . $name], + 'translate' => false + ]); + } + + return $this->data()->$name(...$arguments); + } + + /** + * Writes all changes to the session to the session store + * + * @return void + */ + public function commit() + { + // nothing to do if nothing changed or the session has been just created or destroyed + if ($this->writeMode !== true || $this->tokenExpiry === null || $this->destroyed === true) { + return; + } + + // collect all data + if ($this->newSession) { + // the token has changed + // we are writing to the old session: it only gets the reference to the new session + // and a shortened expiry time (30 second grace period) + $data = [ + 'startTime' => $this->startTime(), + 'expiryTime' => time() + 30, + 'newSession' => $this->newSession + ]; + } else { + $data = [ + 'startTime' => $this->startTime(), + 'expiryTime' => $this->expiryTime(), + 'duration' => $this->duration(), + 'timeout' => $this->timeout(), + 'lastActivity' => $this->lastActivity, + 'renewable' => $this->renewable(), + 'data' => $this->data()->get() + ]; + } + + // encode the data and attach an HMAC + $data = serialize($data); + $data = hash_hmac('sha256', $data, $this->tokenKey) . "\n" . $data; + + // store the data + $this->sessions->store()->set($this->tokenExpiry, $this->tokenId, $data); + $this->sessions->store()->unlock($this->tokenExpiry, $this->tokenId); + $this->writeMode = false; + } + + /** + * Entirely destroys the session + * + * @return void + */ + public function destroy() + { + // no need to destroy new or destroyed sessions + if ($this->tokenExpiry === null || $this->destroyed === true) { + return; + } + + // remove session file + $this->sessions->store()->destroy($this->tokenExpiry, $this->tokenId); + $this->destroyed = true; + $this->writeMode = false; + $this->needsRetransmission = false; + + // remove cookie + if ($this->mode === 'cookie') { + Cookie::remove($this->sessions->cookieName()); + } + } + + /** + * Renews the session with the same session duration + * Renewing also regenerates the session token + * + * @return void + */ + public function renew() + { + if ($this->renewable() !== true) { + throw new LogicException([ + 'key' => 'session.notRenewable', + 'fallback' => 'Cannot renew a session that is not renewable, call $session->renewable(true) first', + 'translate' => false, + ]); + } + + $this->prepareForWriting(); + $this->expiryTime = time() + $this->duration(); + $this->regenerateTokenIfNotNew(); + } + + /** + * Regenerates the session token + * The old token will keep its validity for a 30 second grace period + * + * @return void + */ + public function regenerateToken() + { + // don't do anything for destroyed sessions + if ($this->destroyed === true) { + return; + } + + $this->prepareForWriting(); + + // generate new token + $tokenExpiry = $this->expiryTime; + $tokenId = $this->sessions->store()->createId($tokenExpiry); + $tokenKey = bin2hex(random_bytes(32)); + + // mark the old session as moved if there is one + if ($this->tokenExpiry !== null) { + $this->newSession = $tokenExpiry . '.' . $tokenId; + $this->commit(); + + // we are now in the context of the new session + $this->newSession = null; + } + + // set new data as instance vars + $this->tokenExpiry = $tokenExpiry; + $this->tokenId = $tokenId; + $this->tokenKey = $tokenKey; + + // the new session needs to be written for the first time + $this->writeMode = true; + + // (re)transmit session token + if ($this->mode === 'cookie') { + Cookie::set($this->sessions->cookieName(), $this->token(), [ + 'lifetime' => $this->tokenExpiry, + 'path' => Url::index(['host' => null, 'trailingSlash' => true]), + 'secure' => Url::scheme() === 'https', + 'httpOnly' => true + ]); + } else { + $this->needsRetransmission = true; + } + + // update cache of the Sessions instance with the new token + $this->sessions->updateCache($this); + } + + /** + * Returns whether the session token needs to be retransmitted to the client + * Only relevant in header and manual modes + * + * @return bool + */ + public function needsRetransmission(): bool + { + return $this->needsRetransmission; + } + + /** + * Ensures that all pending changes are written to disk before the object is destructed + */ + public function __destruct() + { + $this->commit(); + } + + /** + * Initially generates the token for new sessions + * Used internally + * + * @return void + */ + public function ensureToken() + { + if ($this->tokenExpiry === null) { + $this->regenerateToken(); + } + } + + /** + * Puts the session into write mode by acquiring a lock and reloading the data + * Used internally + * + * @return void + */ + public function prepareForWriting() + { + // verify that we need to get into write mode: + // - new sessions are only written to if the token has explicitly been ensured + // using $session->ensureToken() -> lazy session creation + // - destroyed sessions are never written to + // - no need to lock and re-init if we are already in write mode + if ($this->tokenExpiry === null || $this->destroyed === true || $this->writeMode === true) { + return; + } + + // don't allow writing for read-only sessions + // (only the case for moved sessions) + if ($this->tokenKey === null) { + throw new LogicException([ + 'key' => 'session.readonly', + 'data' => ['token' => $this->token()], + 'fallback' => 'Session "' . $this->token() . '" is currently read-only because it was accessed via an old session token', + 'translate' => false + ]); + } + + $this->sessions->store()->lock($this->tokenExpiry, $this->tokenId); + $this->init(); + $this->writeMode = true; + } + + /** + * Parses a token string into its parts and sets them as instance vars + * + * @param string $token Session token + * @param bool $withoutKey If true, $token is passed without key + * @return void + */ + protected function parseToken(string $token, bool $withoutKey = false) + { + // split the token into its parts + $parts = explode('.', $token); + + // only continue if the token has exactly the right amount of parts + $expectedParts = ($withoutKey === true)? 2 : 3; + if (count($parts) !== $expectedParts) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::parseToken', 'argument' => '$token'], + 'translate' => false + ]); + } + + $tokenExpiry = (int)$parts[0]; + $tokenId = $parts[1]; + $tokenKey = ($withoutKey === true)? null : $parts[2]; + + // verify that all parts were parsed correctly using reassembly + $expectedToken = $tokenExpiry . '.' . $tokenId; + if ($withoutKey === false) { + $expectedToken .= '.' . $tokenKey; + } + if ($expectedToken !== $token) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::parseToken', 'argument' => '$token'], + 'translate' => false + ]); + } + + $this->tokenExpiry = $tokenExpiry; + $this->tokenId = $tokenId; + $this->tokenKey = $tokenKey; + } + + /** + * Makes sure that the given value is a valid timestamp + * + * @param string|int $time Timestamp or date string (must be supported by `strtotime()`) + * @param int $now Timestamp to use as a base for the calculation of relative dates + * @return int Timestamp value + */ + protected static function timeToTimestamp($time, int $now = null): int + { + // default to current time as $now + if (!is_int($now)) { + $now = time(); + } + + // convert date strings to a timestamp first + if (is_string($time)) { + $time = strtotime($time, $now); + } + + // now make sure that we have a valid timestamp + if (is_int($time)) { + return $time; + } else { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::timeToTimestamp', 'argument' => '$time'], + 'translate' => false + ]); + } + } + + /** + * Loads the session data from the session store + * + * @return void + */ + protected function init() + { + // sessions that are new, written to or that have been destroyed should never be initialized + if ($this->tokenExpiry === null || $this->writeMode === true || $this->destroyed === true) { + // unexpected error that shouldn't occur + throw new Exception(['translate' => false]); // @codeCoverageIgnore + } + + // make sure that the session exists + if ($this->sessions->store()->exists($this->tokenExpiry, $this->tokenId) !== true) { + throw new NotFoundException([ + 'key' => 'session.notFound', + 'data' => ['token' => $this->token()], + 'fallback' => 'Session "' . $this->token() . '" does not exist', + 'translate' => false, + 'httpCode' => 404 + ]); + } + + // get the session data from the store + $data = $this->sessions->store()->get($this->tokenExpiry, $this->tokenId); + + // verify HMAC + // skip if we don't have the key (only the case for moved sessions) + $hmac = Str::before($data, "\n"); + $data = trim(Str::after($data, "\n")); + if ($this->tokenKey !== null && hash_equals(hash_hmac('sha256', $data, $this->tokenKey), $hmac) !== true) { + throw new LogicException([ + 'key' => 'session.invalid', + 'data' => ['token' => $this->token()], + 'fallback' => 'Session "' . $this->token() . '" is invalid', + 'translate' => false, + 'httpCode' => 500 + ]); + } + + // decode the serialized data + try { + $data = unserialize($data); + } catch (Throwable $e) { + throw new LogicException([ + 'key' => 'session.invalid', + 'data' => ['token' => $this->token()], + 'fallback' => 'Session "' . $this->token() . '" is invalid', + 'translate' => false, + 'httpCode' => 500, + 'previous' => $e + ]); + } + + // verify start and expiry time + if (time() < $data['startTime'] || time() > $data['expiryTime']) { + throw new LogicException([ + 'key' => 'session.invalid', + 'data' => ['token' => $this->token()], + 'fallback' => 'Session "' . $this->token() . '" is invalid', + 'translate' => false, + 'httpCode' => 500 + ]); + } + + // follow to the new session if there is one + if (isset($data['newSession'])) { + $this->parseToken($data['newSession'], true); + $this->init(); + return; + } + + // verify timeout + if (is_int($data['timeout'])) { + if (time() - $data['lastActivity'] > $data['timeout']) { + throw new LogicException([ + 'key' => 'session.invalid', + 'data' => ['token' => $this->token()], + 'fallback' => 'Session "' . $this->token() . '" is invalid', + 'translate' => false, + 'httpCode' => 500 + ]); + } + + // set a new activity timestamp, but only every few minutes for better performance + // don't do this if another call to init() is already doing it to prevent endless loops; + // also don't do this for read-only sessions + if ($this->updatingLastActivity === false && $this->tokenKey !== null && time() - $data['lastActivity'] > $data['timeout'] / 15) { + $this->updatingLastActivity = true; + $this->prepareForWriting(); + + // the remaining init steps have been done by prepareForWriting() + $this->lastActivity = time(); + $this->updatingLastActivity = false; + return; + } + } + + // (re)initialize all instance variables + $this->startTime = $data['startTime']; + $this->expiryTime = $data['expiryTime']; + $this->duration = $data['duration']; + $this->timeout = $data['timeout']; + $this->lastActivity = $data['lastActivity']; + $this->renewable = $data['renewable']; + + // reload data into existing object to avoid breaking memory references + if (is_a($this->data, 'Kirby\Session\SessionData')) { + $this->data()->reload($data['data']); + } else { + $this->data = new SessionData($this, $data['data']); + } + } + + /** + * Regenerate session token, but only if there is already one + * + * @return void + */ + protected function regenerateTokenIfNotNew() + { + if ($this->tokenExpiry !== null) { + $this->regenerateToken(); + } + } + + /** + * Automatically renews the session if possible and necessary + * + * @return void + */ + protected function autoRenew() + { + // check if the session needs renewal at all + if ($this->needsRenewal() !== true) { + return; + } + + // re-load the session and check again to make sure that no other thread + // already renewed the session in the meantime + $this->prepareForWriting(); + if ($this->needsRenewal() === true) { + $this->renew(); + } + } + + /** + * Checks if the session can be renewed and if the last renewal + * was more than half a session duration ago + * + * @return bool + */ + protected function needsRenewal(): bool + { + return $this->renewable() === true && $this->expiryTime() - time() < $this->duration() / 2; + } +} diff --git a/kirby/src/Session/SessionData.php b/kirby/src/Session/SessionData.php new file mode 100755 index 0000000..a6aaf5e --- /dev/null +++ b/kirby/src/Session/SessionData.php @@ -0,0 +1,255 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class SessionData +{ + protected $session; + protected $data; + + /** + * Creates a new SessionData instance + * + * @codeCoverageIgnore + * @param \Kirby\Session\Session $session Session object this data belongs to + * @param array $data Currently stored session data + */ + public function __construct(Session $session, array $data) + { + $this->session = $session; + $this->data = $data; + } + + /** + * Sets one or multiple session values by key + * + * @param string|array $key The key to define or a key-value array with multiple values + * @param mixed $value The value for the passed key (only if one $key is passed) + * @return void + */ + public function set($key, $value = null) + { + $this->session->ensureToken(); + $this->session->prepareForWriting(); + + if (is_string($key)) { + $this->data[$key] = $value; + } elseif (is_array($key)) { + $this->data = array_merge($this->data, $key); + } else { + throw new InvalidArgumentException([ + 'data' => ['method' => 'SessionData::set', 'argument' => 'key'], + 'translate' => false + ]); + } + } + + /** + * Increments one or multiple session values by a specified amount + * + * @param string|array $key The key to increment or an array with multiple keys + * @param int $by Increment by which amount? + * @param int $max Maximum amount (value is not incremented further) + * @return void + */ + public function increment($key, int $by = 1, $max = null) + { + if ($max !== null && !is_int($max)) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'SessionData::increment', 'argument' => 'max'], + 'translate' => false + ]); + } + + if (is_string($key)) { + // make sure we have the correct values before getting + $this->session->prepareForWriting(); + + $value = $this->get($key, 0); + + if (!is_int($value)) { + throw new LogicException([ + 'key' => 'session.data.increment.nonInt', + 'data' => ['key' => $key], + 'fallback' => 'Session value "' . $key . '" is not an integer and cannot be incremented', + 'translate' => false + ]); + } + + // increment the value, but ensure $max constraint + if (is_int($max) && $value + $by > $max) { + // set the value to $max + // but not if the current $value is already larger than $max + $value = max($value, $max); + } else { + $value += $by; + } + + $this->set($key, $value); + } elseif (is_array($key)) { + foreach ($key as $k) { + $this->increment($k, $by, $max); + } + } else { + throw new InvalidArgumentException([ + 'data' => ['method' => 'SessionData::increment', 'argument' => 'key'], + 'translate' => false + ]); + } + } + + /** + * Decrements one or multiple session values by a specified amount + * + * @param string|array $key The key to decrement or an array with multiple keys + * @param int $by Decrement by which amount? + * @param int $min Minimum amount (value is not decremented further) + * @return void + */ + public function decrement($key, int $by = 1, $min = null) + { + if ($min !== null && !is_int($min)) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'SessionData::decrement', 'argument' => 'min'], + 'translate' => false + ]); + } + + if (is_string($key)) { + // make sure we have the correct values before getting + $this->session->prepareForWriting(); + + $value = $this->get($key, 0); + + if (!is_int($value)) { + throw new LogicException([ + 'key' => 'session.data.decrement.nonInt', + 'data' => ['key' => $key], + 'fallback' => 'Session value "' . $key . '" is not an integer and cannot be decremented', + 'translate' => false + ]); + } + + // decrement the value, but ensure $min constraint + if (is_int($min) && $value - $by < $min) { + // set the value to $min + // but not if the current $value is already smaller than $min + $value = min($value, $min); + } else { + $value -= $by; + } + + $this->set($key, $value); + } elseif (is_array($key)) { + foreach ($key as $k) { + $this->decrement($k, $by, $min); + } + } else { + throw new InvalidArgumentException([ + 'data' => ['method' => 'SessionData::decrement', 'argument' => 'key'], + 'translate' => false + ]); + } + } + + /** + * Returns one or all session values by key + * + * @param string|null $key The key to get or null for the entire data array + * @param mixed $default Optional default value to return if the key is not defined + * @return mixed + */ + public function get($key = null, $default = null) + { + if (is_string($key)) { + return $this->data[$key] ?? $default; + } elseif ($key === null) { + return $this->data; + } else { + throw new InvalidArgumentException([ + 'data' => ['method' => 'SessionData::get', 'argument' => 'key'], + 'translate' => false + ]); + } + } + + /** + * Retrieves a value and removes it afterwards + * + * @param string $key The key to get + * @param mixed $default Optional default value to return if the key is not defined + * @return mixed + */ + public function pull(string $key, $default = null) + { + // make sure we have the correct value before getting + // we do this here (but not in get) as we need to write anyway + $this->session->prepareForWriting(); + + $value = $this->get($key, $default); + $this->remove($key); + return $value; + } + + /** + * Removes one or multiple session values by key + * + * @param string|array $key The key to remove or an array with multiple keys + * @return void + */ + public function remove($key) + { + $this->session->prepareForWriting(); + + if (is_string($key)) { + unset($this->data[$key]); + } elseif (is_array($key)) { + foreach ($key as $k) { + unset($this->data[$k]); + } + } else { + throw new InvalidArgumentException([ + 'data' => ['method' => 'SessionData::remove', 'argument' => 'key'], + 'translate' => false + ]); + } + } + + /** + * Clears all session data + * + * @return void + */ + public function clear() + { + $this->session->prepareForWriting(); + + $this->data = []; + } + + /** + * Reloads the data array with the current session data + * Only used internally + * + * @param array $data Currently stored session data + * @return void + */ + public function reload(array $data) + { + $this->data = $data; + } +} diff --git a/kirby/src/Session/SessionStore.php b/kirby/src/Session/SessionStore.php new file mode 100755 index 0000000..a3a9611 --- /dev/null +++ b/kirby/src/Session/SessionStore.php @@ -0,0 +1,110 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +abstract class SessionStore +{ + /** + * Creates a new session ID with the given expiry time + * + * Needs to make sure that the session does not already exist + * and needs to reserve it by locking it exclusively. + * + * @param int $expiryTime Timestamp + * @return string Randomly generated session ID (without timestamp) + */ + abstract public function createId(int $expiryTime): string; + + /** + * Checks if the given session exists + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @return bool true: session exists, + * false: session doesn't exist + */ + abstract public function exists(int $expiryTime, string $id): bool; + + /** + * Locks the given session exclusively + * + * Needs to throw an Exception on error. + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @return void + */ + abstract public function lock(int $expiryTime, string $id); + + /** + * Removes all locks on the given session + * + * Needs to throw an Exception on error. + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @return void + */ + abstract public function unlock(int $expiryTime, string $id); + + /** + * Returns the stored session data of the given session + * + * Needs to throw an Exception on error. + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @return string + */ + abstract public function get(int $expiryTime, string $id): string; + + /** + * Stores data to the given session + * + * Needs to make sure that the session exists. + * Needs to throw an Exception on error. + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @param string $data Session data to write + * @return void + */ + abstract public function set(int $expiryTime, string $id, string $data); + + /** + * Deletes the given session + * + * Needs to throw an Exception on error. + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @return void + */ + abstract public function destroy(int $expiryTime, string $id); + + /** + * Deletes all expired sessions + * + * Needs to throw an Exception on error. + * + * @return void + */ + abstract public function collectGarbage(); + + /** + * Securely generates a random session ID + * + * @return string Random hex string with 20 bytes + */ + protected static function generateId(): string + { + return bin2hex(random_bytes(10)); + } +} diff --git a/kirby/src/Session/Sessions.php b/kirby/src/Session/Sessions.php new file mode 100755 index 0000000..d947080 --- /dev/null +++ b/kirby/src/Session/Sessions.php @@ -0,0 +1,288 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Sessions +{ + protected $store; + protected $mode; + protected $cookieName; + + protected $cache = []; + + /** + * Creates a new Sessions instance + * + * @param \Kirby\Session\SessionStore|string $store SessionStore object or a path to the storage directory (uses the FileSessionStore) + * @param array $options Optional additional options: + * - `mode`: Default token transmission mode (cookie, header or manual); defaults to `cookie` + * - `cookieName`: Name to use for the session cookie; defaults to `kirby_session` + * - `gcInterval`: How often should the garbage collector be run?; integer or `false` for never; defaults to `100` + */ + public function __construct($store, array $options = []) + { + if (is_string($store)) { + $this->store = new FileSessionStore($store); + } elseif (is_a($store, 'Kirby\Session\SessionStore') === true) { + $this->store = $store; + } else { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Sessions::__construct', 'argument' => 'store'], + 'translate' => false + ]); + } + + $this->mode = $options['mode'] ?? 'cookie'; + $this->cookieName = $options['cookieName'] ?? 'kirby_session'; + $gcInterval = $options['gcInterval'] ?? 100; + + // validate options + if (!in_array($this->mode, ['cookie', 'header', 'manual'])) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Sessions::__construct', 'argument' => '$options[\'mode\']'], + 'translate' => false + ]); + } + if (!is_string($this->cookieName)) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Sessions::__construct', 'argument' => '$options[\'cookieName\']'], + 'translate' => false + ]); + } + + // trigger automatic garbage collection with the given probability + if (is_int($gcInterval) && $gcInterval > 0) { + // convert the interval into a probability between 0 and 1 + $gcProbability = 1 / $gcInterval; + + // generate a random number + $random = mt_rand(1, 10000); + + // $random will be below or equal $gcProbability * 10000 with a probability of $gcProbability + if ($random <= $gcProbability * 10000) { + $this->collectGarbage(); + } + } elseif ($gcInterval !== false) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Sessions::__construct', 'argument' => '$options[\'gcInterval\']'], + 'translate' => false + ]); + } + } + + /** + * Creates a new empty session + * + * @param array $options Optional additional options: + * - `mode`: Token transmission mode (cookie or manual); defaults to default mode of the Sessions instance + * - `startTime`: Time the session starts being valid (date string or timestamp); defaults to `now` + * - `expiryTime`: Time the session expires (date string or timestamp); defaults to `+ 2 hours` + * - `timeout`: Activity timeout in seconds (integer or false for none); defaults to `1800` (half an hour) + * - `renewable`: Should it be possible to extend the expiry date?; defaults to `true` + * @return \Kirby\Session\Session + */ + public function create(array $options = []) + { + // fall back to default mode + if (!isset($options['mode'])) { + $options['mode'] = $this->mode; + } + + return new Session($this, null, $options); + } + + /** + * Returns the specified Session object + * + * @param string $token Session token, either including or without the key + * @param string $mode Optional transmission mode override + * @return \Kirby\Session\Session + */ + public function get(string $token, string $mode = null) + { + if (isset($this->cache[$token])) { + return $this->cache[$token]; + } + + return $this->cache[$token] = new Session($this, $token, ['mode' => $mode ?? $this->mode]); + } + + /** + * Returns the current session based on the configured token transmission mode: + * - In `cookie` mode: Gets the session from the cookie + * - In `header` mode: Gets the session from the `Authorization` request header + * - In `manual` mode: Fails and throws an Exception + * + * @return \Kirby\Session\Session|null Either the current session or null in case there isn't one + */ + public function current() + { + $token = null; + switch ($this->mode) { + case 'cookie': + $token = $this->tokenFromCookie(); + break; + case 'header': + $token = $this->tokenFromHeader(); + break; + case 'manual': + throw new LogicException([ + 'key' => 'session.sessions.manualMode', + 'fallback' => 'Cannot automatically get current session in manual mode', + 'translate' => false, + 'httpCode' => 500 + ]); + break; + default: + // unexpected error that shouldn't occur + throw new Exception(['translate' => false]); // @codeCoverageIgnore + } + + // no token was found, no session + if (!is_string($token)) { + return null; + } + + // token was found, try to get the session + try { + return $this->get($token); + } catch (Throwable $e) { + return null; + } + } + + /** + * Returns the current session using the following detection order without using the configured mode: + * - Tries to get the session from the `Authorization` request header + * - Tries to get the session from the cookie + * - Otherwise returns null + * + * @return \Kirby\Session\Session|null Either the current session or null in case there isn't one + */ + public function currentDetected() + { + $tokenFromHeader = $this->tokenFromHeader(); + $tokenFromCookie = $this->tokenFromCookie(); + + // prefer header token over cookie token + $token = $tokenFromHeader ?? $tokenFromCookie; + + // no token was found, no session + if (!is_string($token)) { + return null; + } + + // token was found, try to get the session + try { + $mode = (is_string($tokenFromHeader))? 'header' : 'cookie'; + return $this->get($token, $mode); + } catch (Throwable $e) { + return null; + } + } + + /** + * Getter for the session store instance + * Used internally + * + * @return \Kirby\Session\SessionStore + */ + public function store() + { + return $this->store; + } + + /** + * Getter for the cookie name + * Used internally + * + * @return string + */ + public function cookieName(): string + { + return $this->cookieName; + } + + /** + * Deletes all expired sessions + * + * If the `gcInterval` is configured, this is done automatically + * on init of the Sessions object. + * + * @return void + */ + public function collectGarbage() + { + $this->store()->collectGarbage(); + } + + /** + * Updates the instance cache with a newly created + * session or a session with a regenerated token + * + * @internal + * @param \Kirby\Session\Session $session Session instance to push to the cache + */ + public function updateCache(Session $session) + { + $this->cache[$session->token()] = $session; + } + + /** + * Returns the auth token from the cookie + * + * @return string|null + */ + protected function tokenFromCookie() + { + $value = Cookie::get($this->cookieName()); + + if (is_string($value)) { + return $value; + } else { + return null; + } + } + + /** + * Returns the auth token from the Authorization header + * + * @return string|null + */ + protected function tokenFromHeader() + { + $request = new Request(); + $headers = $request->headers(); + + // check if the header exists at all + if (!isset($headers['Authorization'])) { + return null; + } + + // check if the header uses the "Session" scheme + $header = $headers['Authorization']; + if (Str::startsWith($header, 'Session ', true) !== true) { + return null; + } + + // return the part after the scheme + return substr($header, 8); + } +} diff --git a/kirby/src/Text/KirbyTag.php b/kirby/src/Text/KirbyTag.php new file mode 100755 index 0000000..60634aa --- /dev/null +++ b/kirby/src/Text/KirbyTag.php @@ -0,0 +1,142 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class KirbyTag +{ + public static $aliases = []; + public static $types = []; + + public $attrs = []; + public $data = []; + public $options = []; + public $type = null; + public $value = null; + + public function __call(string $name, array $arguments = []) + { + return $this->data[$name] ?? $this->$name; + } + + public static function __callStatic(string $type, array $arguments = []) + { + return (new static($type, ...$arguments))->render(); + } + + public function __construct(string $type, string $value = null, array $attrs = [], array $data = [], array $options = []) + { + if (isset(static::$types[$type]) === false) { + if (isset(static::$aliases[$type]) === false) { + throw new InvalidArgumentException('Undefined tag type: ' . $type); + } + + $type = static::$aliases[$type]; + } + + foreach ($attrs as $attrName => $attrValue) { + $attrName = strtolower($attrName); + $this->$attrName = $attrValue; + } + + $this->attrs = $attrs; + $this->data = $data; + $this->options = $options; + $this->$type = $value; + $this->type = $type; + $this->value = $value; + } + + public function __get(string $attr) + { + $attr = strtolower($attr); + return $this->$attr ?? null; + } + + public function attr(string $name, $default = null) + { + $name = strtolower($name); + return $this->$name ?? $default; + } + + public static function factory(...$arguments) + { + return (new static(...$arguments))->render(); + } + + /** + * @param string $string + * @param array $data + * @param array $options + * @return self + */ + public static function parse(string $string, array $data = [], array $options = []) + { + // remove the brackets, extract the first attribute (the tag type) + $tag = trim(rtrim(ltrim($string, '('), ')')); + $type = trim(substr($tag, 0, strpos($tag, ':'))); + $type = strtolower($type); + $attr = static::$types[$type]['attr'] ?? []; + + // the type should be parsed as an attribute, so we add it here + // to the list of possible attributes + array_unshift($attr, $type); + + // extract all attributes + $regex = sprintf('/(%s):/i', implode('|', $attr)); + $search = preg_split($regex, $tag, false, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + + // $search is now an array with alternating keys and values + // convert it to arrays of keys and values + $chunks = array_chunk($search, 2); + $keys = array_column($chunks, 0); + $values = array_map('trim', array_column($chunks, 1)); + + // ensure that there is a value for each key + // otherwise combining won't work + if (count($values) < count($keys)) { + $values[] = ''; + } + + // combine the two arrays to an associative array + $attributes = array_combine($keys, $values); + + // the first attribute is the type attribute + // extract and pass its value separately + $value = array_shift($attributes); + + return new static($type, $value, $attributes, $data, $options); + } + + public function option(string $key, $default = null) + { + return $this->options[$key] ?? $default; + } + + public function render(): string + { + $callback = static::$types[$this->type]['html'] ?? null; + + if (is_a($callback, 'Closure') === true) { + return (string)$callback($this); + } + + throw new BadMethodCallException('Invalid tag render function in tag: ' . $this->type); + } + + public function type(): string + { + return $this->type; + } +} diff --git a/kirby/src/Text/KirbyTags.php b/kirby/src/Text/KirbyTags.php new file mode 100755 index 0000000..08feffc --- /dev/null +++ b/kirby/src/Text/KirbyTags.php @@ -0,0 +1,33 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class KirbyTags +{ + protected static $tagClass = 'Kirby\Text\KirbyTag'; + + public static function parse(string $text = null, array $data = [], array $options = []): string + { + return preg_replace_callback('!(?=[^\]])\([a-z0-9_-]+:.*?\)!is', function ($match) use ($data, $options) { + try { + return static::$tagClass::parse($match[0], $data, $options)->render(); + } catch (Exception $e) { + return $match[0]; + } + }, $text); + } +} diff --git a/kirby/src/Text/Markdown.php b/kirby/src/Text/Markdown.php new file mode 100755 index 0000000..98c07c4 --- /dev/null +++ b/kirby/src/Text/Markdown.php @@ -0,0 +1,79 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Markdown +{ + /** + * Array with all configured options + * for the parser + * + * @var array + */ + protected $options = []; + + /** + * Returns default values for all + * available parser options + * + * @return array + */ + public function defaults(): array + { + return [ + 'extra' => false, + 'breaks' => true + ]; + } + + /** + * Creates a new Markdown parser + * with the given options + * + * @param array $options + */ + public function __construct(array $options = []) + { + $this->options = array_merge($this->defaults(), $options); + } + + /** + * Parses the given text and returns the HTML + * + * @param string $text + * @param bool $inline + * @return string + */ + public function parse(string $text, bool $inline = false): string + { + if ($this->options['extra'] === true) { + $parser = new ParsedownExtra(); + } else { + $parser = new Parsedown(); + } + + $parser->setBreaksEnabled($this->options['breaks']); + + if ($inline === true) { + return @$parser->line($text); + } else { + return @$parser->text($text); + } + } +} diff --git a/kirby/src/Text/SmartyPants.php b/kirby/src/Text/SmartyPants.php new file mode 100755 index 0000000..50d70cb --- /dev/null +++ b/kirby/src/Text/SmartyPants.php @@ -0,0 +1,129 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class SmartyPants +{ + /** + * Array with all configured options + * for the parser + * + * @var array + */ + protected $options = []; + + /** + * Michelf's parser object + * + * @var SmartyPantsTypographer + */ + protected $parser; + + /** + * Returns default values for all + * available parser options + * + * @return array + */ + public function defaults(): array + { + return [ + 'attr' => 1, + 'doublequote.open' => '“', + 'doublequote.close' => '”', + 'doublequote.low' => '„', + 'singlequote.open' => '‘', + 'singlequote.close' => '’', + 'backtick.doublequote.open' => '“', + 'backtick.doublequote.close' => '”', + 'backtick.singlequote.open' => '‘', + 'backtick.singlequote.close' => '’', + 'emdash' => '—', + 'endash' => '–', + 'ellipsis' => '…', + 'space' => '(?: | | |�*160;|�*[aA]0;)', + 'space.emdash' => ' ', + 'space.endash' => ' ', + 'space.colon' => ' ', + 'space.semicolon' => ' ', + 'space.marks' => ' ', + 'space.frenchquote' => ' ', + 'space.thousand' => ' ', + 'space.unit' => ' ', + 'guillemet.leftpointing' => '«', + 'guillemet.rightpointing' => '»', + 'geresh' => '׳', + 'gershayim' => '״', + 'skip' => 'pre|code|kbd|script|style|math', + ]; + } + + /** + * Creates a new SmartyPants parser + * with the given options + * + * @param array $options + */ + public function __construct(array $options = []) + { + $this->options = array_merge($this->defaults(), $options); + $this->parser = new SmartyPantsTypographer($this->options['attr']); + + // configuration + $this->parser->smart_doublequote_open = $this->options['doublequote.open']; + $this->parser->smart_doublequote_close = $this->options['doublequote.close']; + $this->parser->smart_singlequote_open = $this->options['singlequote.open']; + $this->parser->smart_singlequote_close = $this->options['singlequote.close']; + $this->parser->backtick_doublequote_open = $this->options['backtick.doublequote.open']; + $this->parser->backtick_doublequote_close = $this->options['backtick.doublequote.close']; + $this->parser->backtick_singlequote_open = $this->options['backtick.singlequote.open']; + $this->parser->backtick_singlequote_close = $this->options['backtick.singlequote.close']; + $this->parser->em_dash = $this->options['emdash']; + $this->parser->en_dash = $this->options['endash']; + $this->parser->ellipsis = $this->options['ellipsis']; + $this->parser->tags_to_skip = $this->options['skip']; + $this->parser->space_emdash = $this->options['space.emdash']; + $this->parser->space_endash = $this->options['space.endash']; + $this->parser->space_colon = $this->options['space.colon']; + $this->parser->space_semicolon = $this->options['space.semicolon']; + $this->parser->space_marks = $this->options['space.marks']; + $this->parser->space_frenchquote = $this->options['space.frenchquote']; + $this->parser->space_thousand = $this->options['space.thousand']; + $this->parser->space_unit = $this->options['space.unit']; + $this->parser->doublequote_low = $this->options['doublequote.low']; + $this->parser->guillemet_leftpointing = $this->options['guillemet.leftpointing']; + $this->parser->guillemet_rightpointing = $this->options['guillemet.rightpointing']; + $this->parser->geresh = $this->options['geresh']; + $this->parser->gershayim = $this->options['gershayim']; + $this->parser->space = $this->options['space']; + } + + /** + * Parses the given text + * + * @param string $text + * @return string + */ + public function parse(string $text): string + { + // prepare the text + $text = str_replace('"', '"', $text); + + // parse the text + return $this->parser->transform($text); + } +} diff --git a/kirby/src/Toolkit/A.php b/kirby/src/Toolkit/A.php new file mode 100755 index 0000000..9d03da5 --- /dev/null +++ b/kirby/src/Toolkit/A.php @@ -0,0 +1,605 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class A +{ + /** + * Appends the given array + * + * @param array $array + * @param array $append + * @return array + */ + public static function append(array $array, array $append): array + { + return $array + $append; + } + + /** + * Gets an element of an array by key + * + * + * $array = [ + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ]; + * + * echo A::get($array, 'cat'); + * // output: 'miao' + * + * echo A::get($array, 'elephant', 'shut up'); + * // output: 'shut up' + * + * $catAndDog = A::get($array, ['cat', 'dog']); + * // result: ['cat' => 'miao', 'dog' => 'wuff']; + * + * + * @param array $array The source array + * @param mixed $key The key to look for + * @param mixed $default Optional default value, which should be + * returned if no element has been found + * @return mixed + */ + public static function get($array, $key, $default = null) + { + if (is_array($array) === false) { + return $array; + } + + // return the entire array if the key is null + if ($key === null) { + return $array; + } + + // get an array of keys + if (is_array($key) === true) { + $result = []; + foreach ($key as $k) { + $result[$k] = static::get($array, $k, $default); + } + return $result; + } + + if (isset($array[$key]) === true) { + return $array[$key]; + } + + // support dot notation + if (strpos($key, '.') !== false) { + $keys = explode('.', $key); + $firstKey = array_shift($keys); + + if (isset($array[$firstKey]) === false) { + $currentKey = $firstKey; + + while ($innerKey = array_shift($keys)) { + $currentKey = $currentKey . '.' . $innerKey; + + if (isset($array[$currentKey]) === true && is_array($array[$currentKey])) { + return static::get($array[$currentKey], implode('.', $keys), $default); + } + } + + return $default; + } + + if (is_array($array[$firstKey]) === true) { + return static::get($array[$firstKey], implode('.', $keys), $default); + } + + return $default; + } + + return $default; + } + + /** + * @param mixed $value + * @param mixed $separator + * @return string + */ + public static function join($value, $separator = ', ') + { + if (is_string($value) === true) { + return $value; + } + return implode($separator, $value); + } + + const MERGE_OVERWRITE = 0; + const MERGE_APPEND = 1; + const MERGE_REPLACE = 2; + + /** + * Merges arrays recursively + * + * @param array $array1 + * @param array $array2 + * @param bool $mode Behavior for elements with numeric keys; + * A::MERGE_APPEND: elements are appended, keys are reset; + * A::MERGE_OVERWRITE: elements are overwritten, keys are preserved + * A::MERGE_REPLACE: non-associative arrays are completely replaced + * @return array + */ + public static function merge($array1, $array2, $mode = A::MERGE_APPEND) + { + $merged = $array1; + + if (static::isAssociative($array1) === false && $mode === static::MERGE_REPLACE) { + return $array2; + } + + foreach ($array2 as $key => $value) { + + // append to the merged array, don't overwrite numeric keys + if (is_int($key) === true && $mode == static::MERGE_APPEND) { + $merged[] = $value; + + // recursively merge the two array values + } elseif (is_array($value) === true && isset($merged[$key]) === true && is_array($merged[$key]) === true) { + $merged[$key] = static::merge($merged[$key], $value, $mode); + + // simply overwrite with the value from the second array + } else { + $merged[$key] = $value; + } + } + + if ($mode == static::MERGE_APPEND) { + // the keys don't make sense anymore, reset them + // array_merge() is the simplest way to renumber + // arrays that have both numeric and string keys; + // besides the keys, nothing changes here + $merged = array_merge($merged, []); + } + + return $merged; + } + + /** + * Plucks a single column from an array + * + * + * $array[] = [ + * 'id' => 1, + * 'username' => 'homer', + * ]; + * + * $array[] = [ + * 'id' => 2, + * 'username' => 'marge', + * ]; + * + * $array[] = [ + * 'id' => 3, + * 'username' => 'lisa', + * ]; + * + * var_dump(A::pluck($array, 'username')); + * // result: ['homer', 'marge', 'lisa']; + * + * + * @param array $array The source array + * @param string $key The key name of the column to extract + * @return array The result array with all values + * from that column. + */ + public static function pluck(array $array, string $key) + { + $output = []; + foreach ($array as $a) { + if (isset($a[$key]) === true) { + $output[] = $a[$key]; + } + } + + return $output; + } + + /** + * Prepends the given array + * + * @param array $array + * @param array $prepend + * @return array + */ + public static function prepend(array $array, array $prepend): array + { + return $prepend + $array; + } + + /** + * Shuffles an array and keeps the keys + * + * + * $array = [ + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ]; + * + * $shuffled = A::shuffle($array); + * // output: [ + * // 'dog' => 'wuff', + * // 'cat' => 'miao', + * // 'bird' => 'tweet' + * // ]; + * + * + * @param array $array The source array + * @return array The shuffled result array + */ + public static function shuffle(array $array): array + { + $keys = array_keys($array); + $new = []; + + shuffle($keys); + + // resort the array + foreach ($keys as $key) { + $new[$key] = $array[$key]; + } + + return $new; + } + + /** + * Returns the first element of an array + * + * + * $array = [ + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ]; + * + * $first = A::first($array); + * // first: 'miao' + * + * + * @param array $array The source array + * @return mixed The first element + */ + public static function first(array $array) + { + return array_shift($array); + } + + /** + * Returns the last element of an array + * + * + * $array = [ + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ]; + * + * $last = A::last($array); + * // last: 'tweet' + * + * + * @param array $array The source array + * @return mixed The last element + */ + public static function last(array $array) + { + return array_pop($array); + } + + /** + * Fills an array up with additional elements to certain amount. + * + * + * $array = [ + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ]; + * + * $result = A::fill($array, 5, 'elephant'); + * + * // result: [ + * // 'cat', + * // 'dog', + * // 'bird', + * // 'elephant', + * // 'elephant', + * // ]; + * + * + * @param array $array The source array + * @param int $limit The number of elements the array should + * contain after filling it up. + * @param mixed $fill The element, which should be used to + * fill the array + * @return array The filled-up result array + */ + public static function fill(array $array, int $limit, $fill = 'placeholder'): array + { + if (count($array) < $limit) { + $diff = $limit - count($array); + for ($x = 0; $x < $diff; $x++) { + $array[] = $fill; + } + } + return $array; + } + + /** + * Move an array item to a new index + * + * @param array $array + * @param int $from + * @param int $to + * @return array + */ + public static function move(array $array, int $from, int $to): array + { + $total = count($array); + + if ($from >= $total || $from < 0) { + throw new Exception('Invalid "from" index'); + } + + if ($to >= $total || $to < 0) { + throw new Exception('Invalid "to" index'); + } + + // remove the item from the array + $item = array_splice($array, $from, 1); + + // inject it at the new position + array_splice($array, $to, 0, $item); + + return $array; + } + + /** + * Checks for missing elements in an array + * + * This is very handy to check for missing + * user values in a request for example. + * + * + * $array = [ + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ]; + * + * $required = ['cat', 'elephant']; + * + * $missng = A::missing($array, $required); + * // missing: [ + * // 'elephant' + * // ]; + * + * + * @param array $array The source array + * @param array $required An array of required keys + * @return array An array of missing fields. If this + * is empty, nothing is missing. + */ + public static function missing(array $array, array $required = []): array + { + $missing = []; + foreach ($required as $r) { + if (isset($array[$r]) === false) { + $missing[] = $r; + } + } + return $missing; + } + + /** + * Sorts a multi-dimensional array by a certain column + * + * + * $array[0] = [ + * 'id' => 1, + * 'username' => 'mike', + * ]; + * + * $array[1] = [ + * 'id' => 2, + * 'username' => 'peter', + * ]; + * + * $array[3] = [ + * 'id' => 3, + * 'username' => 'john', + * ]; + * + * $sorted = A::sort($array, 'username ASC'); + * // Array + * // ( + * // [0] => Array + * // ( + * // [id] => 3 + * // [username] => john + * // ) + * // [1] => Array + * // ( + * // [id] => 1 + * // [username] => mike + * // ) + * // [2] => Array + * // ( + * // [id] => 2 + * // [username] => peter + * // ) + * // ) + * + * + * + * @param array $array The source array + * @param string $field The name of the column + * @param string $direction desc (descending) or asc (ascending) + * @param int $method A PHP sort method flag or 'natural' for + * natural sorting, which is not supported in + * PHP by sort flags + * @return array The sorted array + */ + public static function sort(array $array, string $field, string $direction = 'desc', $method = SORT_REGULAR): array + { + $direction = strtolower($direction) == 'desc' ? SORT_DESC : SORT_ASC; + $helper = []; + $result = []; + + // build the helper array + foreach ($array as $key => $row) { + $helper[$key] = $row[$field]; + } + + // natural sorting + if ($direction === SORT_DESC) { + arsort($helper, $method); + } else { + asort($helper, $method); + } + + // rebuild the original array + foreach ($helper as $key => $val) { + $result[$key] = $array[$key]; + } + + return $result; + } + + /** + * Checks wether an array is associative or not + * + * + * $array = ['a', 'b', 'c']; + * + * A::isAssociative($array); + * // returns: false + * + * $array = ['a' => 'a', 'b' => 'b', 'c' => 'c']; + * + * A::isAssociative($array); + * // returns: true + * + * + * @param array $array The array to analyze + * @return bool true: The array is associative false: It's not + */ + public static function isAssociative(array $array): bool + { + return ctype_digit(implode(null, array_keys($array))) === false; + } + + /** + * Returns the average value of an array + * + * @param array $array The source array + * @param int $decimals The number of decimals to return + * @return float The average value + */ + public static function average(array $array, int $decimals = 0): float + { + return round((array_sum($array) / sizeof($array)), $decimals); + } + + /** + * Merges arrays recursively + * + * + * $defaults = [ + * 'username' => 'admin', + * 'password' => 'admin', + * ]; + * + * $options = A::extend($defaults, ['password' => 'super-secret']); + * // returns: [ + * // 'username' => 'admin', + * // 'password' => 'super-secret' + * // ]; + * + * + * @param array ...$arrays + * @return array + */ + public static function extend(...$arrays): array + { + return array_merge_recursive(...$arrays); + } + + /** + * Update an array with a second array + * The second array can contain callbacks as values, + * which will get the original values as argument + * + * + * $user = [ + * 'username' => 'homer', + * 'email' => 'homer@simpsons.com' + * ]; + * + * // simple updates + * A::update($user, [ + * 'username' => 'homer j. simpson' + * ]); + * + * // with callback + * A::update($user, [ + * 'username' => function ($username) { + * return $username . ' j. simpson' + * } + * ]); + * + * + * @param array $array + * @param array $update + * @return array + */ + public static function update(array $array, array $update): array + { + foreach ($update as $key => $value) { + if (is_a($value, 'Closure') === true) { + $array[$key] = call_user_func($value, static::get($array, $key)); + } else { + $array[$key] = $value; + } + } + + return $array; + } + + /** + * Wraps the given value in an array + * if it's not an array yet. + * + * @param mixed|null $array + * @return array + */ + public static function wrap($array = null): array + { + if ($array === null) { + return []; + } elseif (is_array($array) === false) { + return [$array]; + } else { + return $array; + } + } +} diff --git a/kirby/src/Toolkit/Collection.php b/kirby/src/Toolkit/Collection.php new file mode 100755 index 0000000..b0787f5 --- /dev/null +++ b/kirby/src/Toolkit/Collection.php @@ -0,0 +1,1296 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Collection extends Iterator implements Countable +{ + /** + * All registered collection filters + * + * @var array + */ + public static $filters = []; + + /** + * Pagination object + * @var Pagination + */ + protected $pagination; + + /** + * Magic getter function + * + * @param string $key + * @param mixed $arguments + * @return mixed + */ + public function __call(string $key, $arguments) + { + return $this->__get($key); + } + + /** + * Constructor + * + * @param array $data + */ + public function __construct(array $data = []) + { + $this->set($data); + } + + /** + * Improve var_dump() output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->keys(); + } + + /** + * Low-level getter for elements + * + * @param mixed $key + * @return mixed + */ + public function __get($key) + { + if (isset($this->data[$key])) { + return $this->data[$key]; + } + + return $this->data[strtolower($key)] ?? null; + } + + /** + * Low-level setter for elements + * + * @param string $key string or array + * @param mixed $value + */ + public function __set(string $key, $value) + { + $this->data[strtolower($key)] = $value; + return $this; + } + + /** + * Makes it possible to echo the entire object + * + * @return string + */ + public function __toString(): string + { + return $this->toString(); + } + + /** + * Low-level element remover + * + * @param mixed $key the name of the key + */ + public function __unset($key) + { + unset($this->data[$key]); + } + + /** + * Appends an element + * + * @param mixed $key + * @param mixed $item + * @return \Kirby\Toolkit\Collection + */ + public function append(...$args) + { + if (count($args) === 1) { + $this->data[] = $args[0]; + } elseif (count($args) > 1) { + $this->set($args[0], $args[1]); + } + + return $this; + } + + /** + * Creates chunks of the same size. + * The last chunk may be smaller + * + * @param int $size Number of elements per chunk + * @return \Kirby\Toolkit\Collection A new collection with an element for each chunk and + * a sub collection in each chunk + */ + public function chunk(int $size) + { + // create a multidimensional array that is chunked with the given + // chunk size keep keys of the elements + $chunks = array_chunk($this->data, $size, true); + + // convert each chunk to a subcollection + $collection = []; + + foreach ($chunks as $items) { + // we clone $this instead of creating a new object because + // different objects may have different constructors + $clone = clone $this; + $clone->data = $items; + + $collection[] = $clone; + } + + // convert the array of chunks to a collection + $result = clone $this; + $result->data = $collection; + + return $result; + } + + /** + * Returns a cloned instance of the collection + * + * @return self + */ + public function clone() + { + return clone $this; + } + + /** + * Getter and setter for the data + * + * @param array $data + * @return array|Collection + */ + public function data(array $data = null) + { + if ($data === null) { + return $this->data; + } + + // clear all previous data + $this->data = []; + + // overwrite the data array + $this->data = $data; + + return $this; + } + + /** + * Clone and remove all elements from the collection + * + * @return \Kirby\Toolkit\Collection + */ + public function empty() + { + $collection = clone $this; + $collection->data = []; + + return $collection; + } + + /** + * Adds all elements to the collection + * + * @param mixed $items + * @return self + */ + public function extend($items) + { + $collection = clone $this; + return $collection->set($items); + } + + /** + * Filters elements by a custom + * filter function or an array of filters + * + * @param Closure $filter + * @return self + */ + public function filter($filter) + { + if (is_callable($filter) === true) { + $collection = clone $this; + $collection->data = array_filter($this->data, $filter); + + return $collection; + } elseif (is_array($filter) === true) { + $collection = $this; + + foreach ($filter as $arguments) { + $collection = $collection->filterBy(...$arguments); + } + + return $collection; + } + + throw new Exception('The filter method needs either an array of filterBy rules or a closure function to be passed as parameter.'); + } + + /** + * Filters elements by one of the + * predefined filter methods. + * + * @param string $field + * @param array ...$args + * @return self + */ + public function filterBy(string $field, ...$args) + { + $operator = '=='; + $test = $args[0] ?? null; + $split = $args[1] ?? false; + + if (is_string($test) === true && isset(static::$filters[$test]) === true) { + $operator = $test; + $test = $args[1] ?? null; + $split = $args[2] ?? false; + } + + if (is_object($test) === true && method_exists($test, '__toString') === true) { + $test = (string)$test; + } + + // get the filter from the filters array + $filter = static::$filters[$operator] ?? null; + + // return an unfiltered list if the filter does not exist + if ($filter === null) { + return $this; + } + + if (is_array($filter) === true) { + $collection = clone $this; + $validator = $filter['validator']; + $strict = $filter['strict'] ?? true; + $method = $strict ? 'filterMatchesAll' : 'filterMatchesAny'; + + foreach ($collection->data as $key => $item) { + $value = $collection->getAttribute($item, $field, $split); + + if ($split !== false) { + if ($this->$method($validator, $value, $test) === false) { + unset($collection->data[$key]); + } + } elseif ($validator($value, $test) === false) { + unset($collection->data[$key]); + } + } + + return $collection; + } + + return $filter(clone $this, $field, $test, $split); + } + + protected function filterMatchesAny($validator, $values, $test): bool + { + foreach ($values as $value) { + if ($validator($value, $test) !== false) { + return true; + } + } + + return false; + } + + protected function filterMatchesAll($validator, $values, $test): bool + { + foreach ($values as $value) { + if ($validator($value, $test) === false) { + return false; + } + } + + return true; + } + + protected function filterMatchesNone($validator, $values, $test): bool + { + $matches = 0; + + foreach ($values as $value) { + if ($validator($value, $test) !== false) { + $matches++; + } + } + + return $matches === 0; + } + + /** + * Find one or multiple elements by id + * + * @param string ...$keys + * @return mixed + */ + public function find(...$keys) + { + if (count($keys) === 1) { + if (is_array($keys[0]) === true) { + $keys = $keys[0]; + } else { + return $this->findByKey($keys[0]); + } + } + + $result = []; + + foreach ($keys as $key) { + if ($item = $this->findByKey($key)) { + if (is_object($item) && method_exists($item, 'id') === true) { + $key = $item->id(); + } + $result[$key] = $item; + } + } + + $collection = clone $this; + $collection->data = $result; + return $collection; + } + + /** + * Find a single element by an attribute and its value + * + * @param string $attribute + * @param mixed $value + * @return mixed|null + */ + public function findBy(string $attribute, $value) + { + foreach ($this->data as $item) { + if ($this->getAttribute($item, $attribute) == $value) { + return $item; + } + } + return null; + } + + /** + * Find a single element by key (id) + * + * @param string $key + * @return mixed + */ + public function findByKey(string $key) + { + return $this->get($key); + } + + /** + * Returns the first element + * + * @return mixed + */ + public function first() + { + $array = $this->data; + return array_shift($array); + } + + /** + * Returns the elements in reverse order + * + * @return \Kirby\Toolkit\Collection + */ + public function flip() + { + $collection = clone $this; + $collection->data = array_reverse($this->data, true); + return $collection; + } + + /** + * Getter + * + * @param mixed $key + * @param mixed $default + * @return mixed + */ + public function get($key, $default = null) + { + return $this->__get($key) ?? $default; + } + + /** + * Extracts an attribute value from the given element + * in the collection. This is useful if elements in the collection + * might be objects, arrays or anything else and you need to + * get the value independently from that. We use it for filterBy. + * + * @param array|object $item + * @param string $attribute + * @param bool $split + * @param mixed $related + * @return mixed + */ + public function getAttribute($item, string $attribute, $split = false, $related = null) + { + $value = $this->{'getAttributeFrom' . gettype($item)}($item, $attribute); + + if ($split !== false) { + return Str::split($value, $split === true ? ',' : $split); + } + + if ($related !== null) { + return Str::toType((string)$value, $related); + } + + return $value; + } + + /** + * @param array $array + * @param string $attribute + * @return mixed + */ + protected function getAttributeFromArray(array $array, string $attribute) + { + return $array[$attribute] ?? null; + } + + /** + * @param object $object + * @param string $attribute + * @return void + */ + protected function getAttributeFromObject($object, string $attribute) + { + return $object->{$attribute}(); + } + + /** + * Groups the elements by a given callback + * + * @param Closure $callback + * @return self A new collection with an element for each group and a subcollection in each group + */ + public function group(Closure $callback) + { + $groups = []; + + foreach ($this->data as $key => $item) { + + // get the value to group by + $value = $callback($item); + + // make sure that there's always a proper value to group by + if (!$value) { + throw new Exception('Invalid grouping value for key: ' . $key); + } + + // make sure we have a proper key for each group + if (is_array($value) === true) { + throw new Exception('You cannot group by arrays or objects'); + } elseif (is_object($value) === true) { + if (method_exists($value, '__toString') === false) { + throw new Exception('You cannot group by arrays or objects'); + } else { + $value = (string)$value; + } + } + + if (isset($groups[$value]) === false) { + // create a new entry for the group if it does not exist yet + $groups[$value] = new static([$key => $item]); + } else { + // add the element to an existing group + $groups[$value]->set($key, $item); + } + } + + return new Collection($groups); + } + + /** + * Groups the elements by a given field + * + * @param string $field + * @param bool $i + * @return \Kirby\Toolkit\Collection A new collection with an element for each group and a subcollection in each group + */ + public function groupBy($field, bool $i = true) + { + if (is_string($field) === false) { + throw new Exception('Cannot group by non-string values. Did you mean to call group()?'); + } + + return $this->group(function ($item) use ($field, $i) { + $value = $this->getAttribute($item, $field); + + // ignore upper/lowercase for group names + return $i === true ? Str::lower($value) : $value; + }); + } + + /** + * Returns a Collection with the intersection of the given elements + * @since 3.3.0 + * + * @param \Kirby\Toolkit\Collection $other + * @return \Kirby\Toolkit\Collection + */ + public function intersection($other) + { + return $other->find($this->keys()); + } + + /** + * Checks if there is an intersection between the given collection and this collection + * @since 3.3.0 + * + * @param \Kirby\Toolkit\Collection $other + * @return bool + */ + public function intersects($other): bool + { + foreach ($this->keys() as $key) { + if ($other->has($key)) { + return true; + } + } + + return false; + } + + /** + * Checks if the number of elements is zero + * + * @return bool + */ + public function isEmpty(): bool + { + return $this->count() === 0; + } + + /** + * Checks if the number of elements is even + * + * @return bool + */ + public function isEven(): bool + { + return $this->count() % 2 === 0; + } + + /** + * Checks if the number of elements is more than zero + * + * @return bool + */ + public function isNotEmpty(): bool + { + return $this->count() > 0; + } + + /** + * Checks if the number of elements is odd + * + * @return bool + */ + public function isOdd(): bool + { + return $this->count() % 2 !== 0; + } + + /** + * Returns the last element + * + * @return mixed + */ + public function last() + { + $array = $this->data; + return array_pop($array); + } + + /** + * Returns a new object with a limited number of elements + * + * @param int $limit The number of elements to return + * @return \Kirby\Toolkit\Collection + */ + public function limit(int $limit) + { + return $this->slice(0, $limit); + } + + /** + * Map a function to each element + * + * @param callable $callback + * @return \Kirby\Toolkit\Collection + */ + public function map(callable $callback) + { + $this->data = array_map($callback, $this->data); + return $this; + } + + /** + * Returns the nth element from the collection + * + * @param int $n + * @return mixed + */ + public function nth(int $n) + { + return array_values($this->data)[$n] ?? null; + } + + /** + * Returns a Collection without the given element(s) + * + * @param string ...$keys any number of keys, passed as individual arguments + * @return \Kirby\Toolkit\Collection + */ + public function not(...$keys) + { + $collection = clone $this; + foreach ($keys as $key) { + unset($collection->data[$key]); + } + return $collection; + } + + /** + * Returns a new object starting from the given offset + * + * @param int $offset The index to start from + * @return \Kirby\Toolkit\Collection + */ + public function offset(int $offset) + { + return $this->slice($offset); + } + + /** + * Add pagination + * + * @param array ...$arguments + * @return \Kirby\Toolkit\Collection a sliced set of data + */ + 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()); + } + + /** + * Get the previously added pagination object + * + * @return \Kirby\Toolkit\Pagination|null + */ + public function pagination() + { + return $this->pagination; + } + + /** + * Extracts all values for a single field into + * a new array + * + * @param string $field + * @param string $split + * @param bool $unique + * @return array + */ + public function pluck(string $field, string $split = null, bool $unique = false): array + { + $result = []; + + foreach ($this->data as $item) { + $row = $this->getAttribute($item, $field); + + if ($split !== null) { + $result = array_merge($result, Str::split($row, $split)); + } else { + $result[] = $row; + } + } + + if ($unique === true) { + $result = array_unique($result); + } + + return array_values($result); + } + + /** + * Prepends an element to the data array + * + * @param mixed $key + * @param mixed $item + * @return self + */ + public function prepend(...$args) + { + if (count($args) === 1) { + array_unshift($this->data, $args[0]); + } elseif (count($args) > 1) { + $data = $this->data; + $this->data = []; + $this->set($args[0], $args[1]); + $this->data += $data; + } + + return $this; + } + + /** + * Runs a combination of filterBy, sortBy, not + * offset, limit and paginate on the collection. + * Any part of the query is optional. + * + * @param array $arguments + * @return self + */ + public function query(array $arguments = []) + { + $result = clone $this; + + if (isset($arguments['not']) === true) { + $result = $result->not(...$arguments['not']); + } + + if (isset($arguments['filterBy']) === true) { + foreach ($arguments['filterBy'] as $filter) { + if (isset($filter['field']) === true && isset($filter['value']) === true) { + $result = $result->filterBy($filter['field'], $filter['operator'] ?? '==', $filter['value']); + } + } + } + + if (isset($arguments['offset']) === true) { + $result = $result->offset($arguments['offset']); + } + + if (isset($arguments['limit']) === true) { + $result = $result->limit($arguments['limit']); + } + + if (isset($arguments['sortBy']) === true) { + if (is_array($arguments['sortBy'])) { + $sort = explode(' ', implode(' ', $arguments['sortBy'])); + } else { + $sort = explode(' ', $arguments['sortBy']); + } + $result = $result->sortBy(...$sort); + } + + if (isset($arguments['paginate']) === true) { + $result = $result->paginate($arguments['paginate']); + } + + return $result; + } + + /** + * Removes an element from the array by key + * + * @param mixed $key the name of the key + */ + public function remove($key) + { + $this->__unset($key); + return $this; + } + + /** + * Adds a new element to the collection + * + * @param mixed $key string or array + * @param mixed $value + * @return self + */ + public function set($key, $value = null) + { + if (is_array($key)) { + foreach ($key as $k => $v) { + $this->__set($k, $v); + } + } else { + $this->__set($key, $value); + } + return $this; + } + + /** + * Shuffle all elements + * + * @return \Kirby\Toolkit\Collection + */ + public function shuffle() + { + $data = $this->data; + $keys = $this->keys(); + shuffle($keys); + + $collection = clone $this; + $collection->data = []; + + foreach ($keys as $key) { + $collection->data[$key] = $data[$key]; + } + + return $collection; + } + + /** + * Returns a slice of the object + * + * @param int $offset The optional index to start the slice from + * @param int $limit The optional number of elements to return + * @return \Kirby\Toolkit\Collection + */ + public function slice(int $offset = 0, int $limit = null) + { + if ($offset === 0 && $limit === null) { + return $this; + } + + $collection = clone $this; + $collection->data = array_slice($this->data, $offset, $limit); + return $collection; + } + + /** + * Get sort arguments from a string + * + * @param string $sortBy + * @return array + */ + public static function sortArgs(string $sortBy): array + { + $sortArgs = Str::split($sortBy, ' '); + + // fill in PHP constants + array_walk($sortArgs, function (string &$value) { + if (Str::startsWith($value, 'SORT_') === true && defined($value) === true) { + $value = constant($value); + } + }); + + return $sortArgs; + } + + /** + * Sorts the elements by any number of fields + * + * @param string|callable $field Field name or value callback to sort by + * @param string $direction asc or desc + * @param int $method The sort flag, SORT_REGULAR, SORT_NUMERIC etc. + * @return Collection + */ + public function sortBy() + { + // there is no need to sort empty collections + if (empty($this->data) === true) { + return $this; + } + + $args = func_get_args(); + $array = $this->data; + $collection = $this->clone(); + + // loop through all method arguments and find sets of fields to sort by + $fields = []; + + foreach ($args as $arg) { + + // get the index of the latest field array inside the $fields array + $currentField = $fields ? count($fields) - 1 : 0; + + // detect the type of argument + // sorting direction + $argLower = is_string($arg) ? strtolower($arg) : null; + + if ($arg === SORT_ASC || $argLower === 'asc') { + $fields[$currentField]['direction'] = SORT_ASC; + } elseif ($arg === SORT_DESC || $argLower === 'desc') { + $fields[$currentField]['direction'] = SORT_DESC; + + // other string: the field name + } elseif (is_string($arg) === true) { + $values = []; + + foreach ($array as $key => $value) { + $value = $collection->getAttribute($value, $arg); + + // make sure that we return something sortable + // but don't convert other scalars (especially numbers) to strings! + $values[$key] = is_scalar($value) === true ? $value : (string)$value; + } + + $fields[] = ['field' => $arg, 'values' => $values]; + + // callable: custom field values + } elseif (is_callable($arg) === true) { + $values = []; + + foreach ($array as $key => $value) { + $value = $arg($value); + + // make sure that we return something sortable + // but don't convert other scalars (especially numbers) to strings! + $values[$key] = is_scalar($value) === true ? $value : (string)$value; + } + + $fields[] = ['field' => null, 'values' => $values]; + + // flags + } else { + $fields[$currentField]['flags'] = $arg; + } + } + + // build the multisort params in the right order + $params = []; + + foreach ($fields as $field) { + $params[] = $field['values'] ?? []; + $params[] = $field['direction'] ?? SORT_ASC; + $params[] = $field['flags'] ?? SORT_NATURAL | SORT_FLAG_CASE; + } + + // check what kind of collection items we have; only check for the first + // item for better performance (we assume that all collection items are + // of the same type) + $firstItem = $collection->first(); + if (is_object($firstItem) === true) { + // avoid the "Nesting level too deep - recursive dependency?" error + // when PHP tries to sort by the objects directly (in case all other + // fields are 100 % equal for some elements) + if (method_exists($firstItem, '__toString') === true) { + // PHP can easily convert the objects to strings, so it should + // compare them as strings instead of as objects to avoid the recursion + $params[] = &$array; + $params[] = SORT_STRING; + } else { + // we can't convert the objects to strings, so we need a fallback: + // custom fictional field that is guaranteed to have a unique value + // for each item; WARNING: may lead to slightly wrong sorting results + // and is therefore only used as a fallback if we don't have another way + $params[] = range(1, count($array)); + $params[] = SORT_ASC; + $params[] = SORT_NUMERIC; + + $params[] = &$array; + } + } else { + // collection items are scalar or array; no correction necessary + $params[] = &$array; + } + + // array_multisort receives $params as separate params + array_multisort(...$params); + + // $array has been overwritten by array_multisort + $collection->data = $array; + return $collection; + } + + /** + * Converts the object into an array + * + * @param Closure $map + * @return array + */ + public function toArray(Closure $map = null): array + { + if ($map !== null) { + return array_map($map, $this->data); + } + + return $this->data; + } + + /** + * Converts the object into a JSON string + * + * @return string + */ + public function toJson(): string + { + return json_encode($this->toArray()); + } + + /** + * Convertes the object to a string + * + * @return string + */ + public function toString(): string + { + return implode('
', $this->keys()); + } + + /** + * Returns an non-associative array + * with all values + * + * @return array + */ + public function values(): array + { + return array_values($this->data); + } + + /** + * The when method only executes the given Closure when the first parameter + * is true. If the first parameter is false, the Closure will not be executed. + * You may pass another Closure as the third parameter to the when method. + * This Closure will execute if the first parameter evaluates as false + * @since 3.3.0 + * + * @param mixed $condition + * @param Closure $callback + * @param Closure $fallback + * @return mixed + */ + public function when($condition, Closure $callback, Closure $fallback = null) + { + if ($condition) { + return $callback->call($this, $condition); + } + + if ($fallback !== null) { + return $fallback->call($this, $condition); + } + + return $this; + } + + /** + * Alias for $this->not() + * + * @param string ...$keys any number of keys, passed as individual arguments + * @return \Kirby\Toolkit\Collection + */ + public function without(...$keys) + { + return $this->not(...$keys); + } +} + +/** + * Equals Filter + */ +Collection::$filters['=='] = function ($collection, $field, $test, $split = false) { + foreach ($collection->data as $key => $item) { + $value = $collection->getAttribute($item, $field, $split, $test); + + if ($split !== false) { + if (in_array($test, $value) === false) { + unset($collection->data[$key]); + } + } elseif ($value !== $test) { + unset($collection->data[$key]); + } + } + + return $collection; +}; + +/** + * Not Equals Filter + */ +Collection::$filters['!='] = function ($collection, $field, $test, $split = false) { + foreach ($collection->data as $key => $item) { + $value = $collection->getAttribute($item, $field, $split, $test); + + if ($split !== false) { + if (in_array($test, $value) === true) { + unset($collection->data[$key]); + } + } elseif ((string)$value == $test) { + unset($collection->data[$key]); + } + } + + return $collection; +}; + +/** + * In Filter + */ +Collection::$filters['in'] = [ + 'validator' => function ($value, $test) { + return in_array($value, $test) === true; + }, + 'strict' => false +]; + +/** + * Not In Filter + */ +Collection::$filters['not in'] = [ + 'validator' => function ($value, $test) { + return in_array($value, $test) === false; + }, +]; + +/** + * Contains Filter + */ +Collection::$filters['*='] = [ + 'validator' => function ($value, $test) { + return strpos($value, $test) !== false; + }, + 'strict' => false +]; + +/** + * Not Contains Filter + */ +Collection::$filters['!*='] = [ + 'validator' => function ($value, $test) { + return strpos($value, $test) === false; + }, +]; + +/** + * More Filter + */ +Collection::$filters['>'] = [ + 'validator' => function ($value, $test) { + return $value > $test; + } +]; + +/** + * Min Filter + */ +Collection::$filters['>='] = [ + 'validator' => function ($value, $test) { + return $value >= $test; + } +]; + +/** + * Less Filter + */ +Collection::$filters['<'] = [ + 'validator' => function ($value, $test) { + return $value < $test; + } +]; + +/** + * Max Filter + */ +Collection::$filters['<='] = [ + 'validator' => function ($value, $test) { + return $value <= $test; + } +]; + +/** + * Ends With Filter + */ +Collection::$filters['$='] = [ + 'validator' => 'V::endsWith', + 'strict' => false, +]; + +/** + * Not Ends With Filter + */ +Collection::$filters['!$='] = [ + 'validator' => function ($value, $test) { + return V::endsWith($value, $test) === false; + } +]; + +/** + * Starts With Filter + */ +Collection::$filters['^='] = [ + 'validator' => 'V::startsWith', + 'strict' => false +]; + +/** + * Not Starts With Filter + */ +Collection::$filters['!^='] = [ + 'validator' => function ($value, $test) { + return V::startsWith($value, $test) === false; + } +]; + +/** + * Between Filter + */ +Collection::$filters['between'] = [ + 'validator' => function ($value, $test) { + return V::between($value, ...$test) === true; + }, + 'strict' => false +]; + +/** + * Match Filter + */ +Collection::$filters['*'] = [ + 'validator' => 'V::match', + 'strict' => false +]; + +/** + * Not Match Filter + */ +Collection::$filters['!*'] = [ + 'validator' => function ($value, $test) { + return V::match($value, $test) === false; + } +]; + +/** + * Max Length Filter + */ +Collection::$filters['maxlength'] = [ + 'validator' => 'V::maxLength', +]; + +/** + * Min Length Filter + */ +Collection::$filters['minlength'] = [ + 'validator' => 'V::minLength' +]; + +/** + * Max Words Filter + */ +Collection::$filters['maxwords'] = [ + 'validator' => 'V::maxWords', +]; + +/** + * Min Words Filter + */ +Collection::$filters['minwords'] = [ + 'validator' => 'V::minWords', +]; diff --git a/kirby/src/Toolkit/Component.php b/kirby/src/Toolkit/Component.php new file mode 100755 index 0000000..2b12488 --- /dev/null +++ b/kirby/src/Toolkit/Component.php @@ -0,0 +1,285 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Component +{ + /** + * Registry for all component mixins + * + * @var array + */ + public static $mixins = []; + + /** + * Registry for all component types + * + * @var array + */ + public static $types = []; + + /** + * An array of all passed attributes + * + * @var array + */ + protected $attrs = []; + + /** + * An array of all computed properties + * + * @var array + */ + protected $computed = []; + + /** + * An array of all registered methods + * + * @var array + */ + protected $methods = []; + + /** + * An array of all component options + * from the component definition + * + * @var array + */ + protected $options = []; + + /** + * An array of all resolved props + * + * @var array + */ + protected $props = []; + + /** + * The component type + * + * @var string + */ + protected $type; + + /** + * Magic caller for defined methods and properties + * + * @param string $name + * @param array $arguments + * @return mixed + */ + public function __call(string $name, array $arguments = []) + { + if (array_key_exists($name, $this->computed) === true) { + return $this->computed[$name]; + } + + if (array_key_exists($name, $this->props) === true) { + return $this->props[$name]; + } + + if (array_key_exists($name, $this->methods) === true) { + return $this->methods[$name]->call($this, ...$arguments); + } + + return $this->$name; + } + + /** + * Creates a new component for the given type + * + * @param string $type + * @param array $attrs + */ + public function __construct(string $type, array $attrs = []) + { + if (isset(static::$types[$type]) === false) { + throw new InvalidArgumentException('Undefined component type: ' . $type); + } + + $this->attrs = $attrs; + $this->options = $options = $this->setup($type); + $this->methods = $methods = $options['methods'] ?? []; + + foreach ($attrs as $attrName => $attrValue) { + $this->$attrName = $attrValue; + } + + if (isset($options['props']) === true) { + $this->applyProps($options['props']); + } + + if (isset($options['computed']) === true) { + $this->applyComputed($options['computed']); + } + + $this->attrs = $attrs; + $this->methods = $methods; + $this->options = $options; + $this->type = $type; + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } + + /** + * Fallback for missing properties to return + * null instead of an error + * + * @param string $attr + * @return null + */ + public function __get(string $attr) + { + return null; + } + + /** + * A set of default options for each component. + * This can be overwritten by extended classes + * to define basic options that should always + * be applied. + * + * @return array + */ + public static function defaults(): array + { + return []; + } + + /** + * Register all defined props and apply the + * passed values. + * + * @param array $props + * @return void + */ + protected function applyProps(array $props): void + { + foreach ($props as $propName => $propFunction) { + if (is_callable($propFunction) === true) { + if (isset($this->attrs[$propName]) === true) { + try { + $this->$propName = $this->props[$propName] = $propFunction->call($this, $this->attrs[$propName]); + } catch (TypeError $e) { + throw new TypeError('Invalid value for "' . $propName . '"'); + } + } else { + try { + $this->$propName = $this->props[$propName] = $propFunction->call($this); + } catch (ArgumentCountError $e) { + throw new ArgumentCountError('Please provide a value for "' . $propName . '"'); + } + } + } else { + $this->$propName = $this->props[$propName] = $propFunction; + } + } + } + + /** + * Register all computed properties and calculate their values. + * This must happen after all props are registered. + * + * @param array $computed + * @return void + */ + protected function applyComputed(array $computed): void + { + foreach ($computed as $computedName => $computedFunction) { + if (is_callable($computedFunction) === true) { + $this->$computedName = $this->computed[$computedName] = $computedFunction->call($this); + } + } + } + + /** + * Load a component definition by type + * + * @param string $type + * @return array + */ + public static function load(string $type): array + { + $definition = static::$types[$type]; + + // load definitions from string + if (is_array($definition) === false) { + static::$types[$type] = $definition = include $definition; + } + + return $definition; + } + + /** + * Loads all options from the component definition + * mixes in the defaults from the defaults method and + * then injects all additional mixins, defined in the + * component options. + * + * @param string $type + * @return array + */ + public static function setup(string $type): array + { + // load component definition + $definition = static::load($type); + + if (isset($definition['extends']) === true) { + // extend other definitions + $options = array_replace_recursive(static::defaults(), static::load($definition['extends']), $definition); + } else { + // inject defaults + $options = array_replace_recursive(static::defaults(), $definition); + } + + // inject mixins + if (isset($options['mixins']) === true) { + foreach ($options['mixins'] as $mixin) { + if (isset(static::$mixins[$mixin]) === true) { + $options = array_replace_recursive(static::$mixins[$mixin], $options); + } + } + } + + return $options; + } + + /** + * Converts all props and computed props to an array + * + * @return array + */ + public function toArray(): array + { + if (is_a($this->options['toArray'] ?? null, 'Closure') === true) { + return $this->options['toArray']->call($this); + } + + $array = array_merge($this->attrs, $this->props, $this->computed); + + ksort($array); + + return $array; + } +} diff --git a/kirby/src/Toolkit/Config.php b/kirby/src/Toolkit/Config.php new file mode 100755 index 0000000..59cabf3 --- /dev/null +++ b/kirby/src/Toolkit/Config.php @@ -0,0 +1,21 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Config extends Silo +{ + /** + * @var array + */ + public static $data = []; +} diff --git a/kirby/src/Toolkit/Controller.php b/kirby/src/Toolkit/Controller.php new file mode 100755 index 0000000..e711769 --- /dev/null +++ b/kirby/src/Toolkit/Controller.php @@ -0,0 +1,66 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Controller +{ + protected $function; + + public function __construct(Closure $function) + { + $this->function = $function; + } + + public function arguments(array $data = []): array + { + $info = new ReflectionFunction($this->function); + $args = []; + + foreach ($info->getParameters() as $parameter) { + $name = $parameter->getName(); + $args[] = $data[$name] ?? null; + } + + return $args; + } + + public function call($bind = null, $data = []) + { + $args = $this->arguments($data); + + if ($bind === null) { + return call_user_func($this->function, ...$args); + } + + return $this->function->call($bind, ...$args); + } + + public static function load(string $file) + { + if (file_exists($file) === false) { + return null; + } + + $function = require $file; + + if (is_a($function, 'Closure') === false) { + return null; + } + + return new static($function); + } +} diff --git a/kirby/src/Toolkit/Dir.php b/kirby/src/Toolkit/Dir.php new file mode 100755 index 0000000..f079b0c --- /dev/null +++ b/kirby/src/Toolkit/Dir.php @@ -0,0 +1,428 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Dir +{ + /** + * Ignore when scanning directories + * + * @var array + */ + public static $ignore = [ + '.', + '..', + '.DS_Store', + '.gitignore', + '.git', + '.svn', + '.htaccess', + 'Thumb.db', + '@eaDir' + ]; + + /** + * 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); + } + } 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; + } + + /** + * 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); + } + + /** + * Create a (symbolic) link to a directory + * + * @param string $source + * @param string $link + * @return bool + */ + public static function link(string $source, string $link): bool + { + Dir::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 + */ + public static function make(string $dir, bool $recursive = true): bool + { + if (empty($dir) === true) { + return false; + } + + if (is_dir($dir) === true) { + return true; + } + + $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 + */ + 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 $format !== null ? $handler($format, $modified) : $modified; + } + + /** + * 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 + * @return mixed + */ + public static function niceSize(string $dir) + { + return F::niceSize(static::size($dir)); + } + + /** + * 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 = $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(function ($item) use ($dir) { + return $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 and all subfolders and files + * + * @param string $dir The path of the directory + * @return mixed + */ + public static function size(string $dir) + { + if (is_dir($dir) === false) { + return false; + } + + $size = 0; + $items = static::read($dir); + + foreach ($items as $item) { + $root = $dir . '/' . $item; + + if (is_dir($root) === true) { + $size += static::size($root); + } elseif (is_file($root) === true) { + $size += F::size($root); + } + } + + 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/Toolkit/Escape.php b/kirby/src/Toolkit/Escape.php new file mode 100755 index 0000000..5b6ebcd --- /dev/null +++ b/kirby/src/Toolkit/Escape.php @@ -0,0 +1,145 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Escape +{ + /** + * Escape common HTML attributes data + * + * This can be used to put untrusted data into typical attribute values + * like width, name, value, etc. + * + * This should not be used for complex attributes like href, src, style, + * or any of the event handlers like onmouseover. + * Use esc($string, 'js') for event handler attributes, esc($string, 'url') + * for src attributes and esc($string, 'css') for style attributes. + * + *
content
+ *
content
+ *
content
+ * + * @param string $string + * @return string + */ + public static function attr($string) + { + return (new Escaper('utf-8'))->escapeHtmlAttr($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 (new Escaper('utf-8'))->escapeCss($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 (new Escaper('utf-8'))->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 (new Escaper('utf-8'))->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 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/F.php b/kirby/src/Toolkit/F.php new file mode 100755 index 0000000..79efb74 --- /dev/null +++ b/kirby/src/Toolkit/F.php @@ -0,0 +1,794 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class F +{ + 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', + 'bmp', + 'gif', + 'eps', + 'ico', + 'jpeg', + 'jpg', + 'jpe', + 'jp2', + 'png', + 'ps', + 'psd', + 'svg', + 'tif', + 'tiff', + 'webp' + ], + 'video' => [ + 'avi', + 'flv', + 'm4v', + 'mov', + 'movie', + 'mpe', + 'mpg', + 'mp4', + 'ogg', + 'ogv', + 'swf', + 'webm', + ], + ]; + + 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 + * + * @param string $file + * @param mixed $fallback + * @return mixed + */ + public static function load(string $file, $fallback = null) + { + if (file_exists($file) === false) { + return $fallback; + } + + $result = include $file; + + if ($fallback !== null && gettype($result) !== gettype($fallback)) { + return $fallback; + } + + return $result; + } + + /** + * 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; + } + + $stat = stat($file); + $mtime = $stat['mtime'] ?? 0; + $ctime = $stat['ctime'] ?? 0; + $modified = max([$mtime, $ctime]); + + if (is_null($format) === true) { + return $modified; + } + + return $handler($format, $modified); + } + + /** + * 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 or a file path + * @return string|int + */ + public static function niceSize($size): string + { + // file mode + if (is_string($size) === true && file_exists($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 + return round($size / pow(1024, ($i = floor(log($size, 1024)))), 2) . ' ' . static::$units[$i]; + } + + /** + * Reads the content of a file + * + * @param string $file The path for the file + * @return string|false + */ + public static function read(string $file) + { + 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) !== 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 + * + * @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); + + if (Str::contains($file, $in) === false) { + return basename($file); + } + + 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 genious.txt'); + * // safe will be ueber-genious.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. + * + * @param mixed $file The path + * @return int + */ + public static function size(string $file): int + { + 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; + } + + /** + * 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 exisiting 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/Toolkit/Facade.php b/kirby/src/Toolkit/Facade.php new file mode 100755 index 0000000..3387a97 --- /dev/null +++ b/kirby/src/Toolkit/Facade.php @@ -0,0 +1,36 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +abstract class Facade +{ + /** + * 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); + } +} diff --git a/kirby/src/Toolkit/File.php b/kirby/src/Toolkit/File.php new file mode 100755 index 0000000..7a43a5c --- /dev/null +++ b/kirby/src/Toolkit/File.php @@ -0,0 +1,340 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class File +{ + /** + * Absolute file path + * + * @var string + */ + protected $root; + + /** + * Constructs a new File object by absolute path + * + * @param string $root Absolute file path + */ + public function __construct(string $root = null) + { + $this->root = $root; + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } + + /** + * 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 self + */ + 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); + } + + /** + * 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()); + } + + /** + * 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; + } + + /** + * 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 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); + } + + /** + * 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 writable + * + * @return bool + */ + public function isWritable(): bool + { + return F::isWritable($this->root); + } + + /** + * Detects the mime type of the file + * + * @return string|null + */ + public function mime() + { + return Mime::type($this->root); + } + + /** + * Get the file's last modification time. + * + * @param string $format + * @param string $handler date or strftime + * @return mixed + */ + public function modified(string $format = null, string $handler = 'date') + { + return F::modified($this->root, $format, $handler); + } + + /** + * Move the file to a new location + * + * @param string $newRoot + * @param bool $overwrite Force overwriting any existing files + * @return self + */ + 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); + } + + /** + * 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 + * + * @return string + */ + public function niceSize(): string + { + return F::niceSize($this->root); + } + + /** + * Reads the file content and returns it. + * + * @return string + */ + 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); + } + + /** + * Changes the name of the file without + * touching the extension + * + * @param string $newName + * @param bool $overwrite Force overwrite existing files + * @return self + */ + 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 . '"'); + } + + return new static($newRoot); + } + + /** + * Returns the given file path + * + * @return string|null + */ + public function root(): ?string + { + return $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 [ + 'root' => $this->root(), + 'hash' => $this->hash(), + 'filename' => $this->filename(), + 'name' => $this->name(), + 'safeName' => F::safeName($this->name()), + 'extension' => $this->extension(), + 'size' => $this->size(), + 'niceSize' => $this->niceSize(), + 'modified' => $this->modified('c'), + 'mime' => $this->mime(), + 'type' => $this->type(), + 'isWritable' => $this->isWritable(), + 'isReadable' => $this->isReadable(), + ]; + } + + /** + * Returns the file type. + * + * @return string|false + */ + public function type() + { + return F::type($this->root); + } + + /** + * 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; + } +} diff --git a/kirby/src/Toolkit/Html.php b/kirby/src/Toolkit/Html.php new file mode 100755 index 0000000..de0b535 --- /dev/null +++ b/kirby/src/Toolkit/Html.php @@ -0,0 +1,536 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Html +{ + /** + * An internal store for a html entities translation table + * + * @var array + */ + public static $entities; + + /** + * Can be used to switch to trailing slashes if required + * + * ```php + * html::$void = ' />' + * ``` + * + * @var string $void + */ + public static $void = '>'; + + /** + * Generic HTML tag generator + * + * @param string $tag + * @param array $arguments + * @return string + */ + public static function __callStatic(string $tag, array $arguments = []): string + { + if (static::isVoid($tag) === true) { + return Html::tag($tag, null, ...$arguments); + } + + return Html::tag($tag, ...$arguments); + } + + /** + * Generates an `a` tag + * + * @param string $href The url for the `a` tag + * @param mixed $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 = null, $text = null, array $attr = []): string + { + if (Str::startsWith($href, 'mailto:')) { + return static::email($href, $text, $attr); + } + + if (Str::startsWith($href, 'tel:')) { + return static::tel($href, $text, $attr); + } + + return static::link($href, $text, $attr); + } + + /** + * Generates a single attribute or a list of attributes + * + * @param string $name mixed string: a single attribute with that name will be generated. array: a list of attributes will be generated. Don't pass a second argument in that case. + * @param string $value if used for a single attribute, pass the content for the attribute here + * @return string the generated html + */ + public static function attr($name, $value = null): string + { + if (is_array($name) === true) { + $attributes = []; + + ksort($name); + + foreach ($name as $key => $val) { + $a = static::attr($key, $val); + + if ($a) { + $attributes[] = $a; + } + } + + return implode(' ', $attributes); + } + + if ($value === null || $value === '' || $value === []) { + return false; + } + + if ($value === ' ') { + return strtolower($name) . '=""'; + } + + if (is_bool($value) === true) { + return $value === true ? strtolower($name) : ''; + } + + if (is_array($value) === true) { + if (isset($value['value'], $value['escape'])) { + $value = $value['escape'] === true ? htmlspecialchars($value['value'], ENT_QUOTES, 'UTF-8') : $value['value']; + } else { + $value = implode(' ', array_filter($value, function ($value) { + return !empty($value) || is_numeric($value); + })); + } + } else { + $value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); + } + + return strtolower($name) . '="' . $value . '"'; + } + + /** + * Converts lines in a string into html breaks + * + * @param string $string + * @return string + */ + public static function breaks(string $string = null): string + { + return nl2br($string); + } + + /** + * Removes all html tags and encoded chars from a string + * + * + * + * echo html::decode('some uber crazy stuff'); + * // output: some uber crazy stuff + * + * + * + * @param string $string + * @return string The html string + */ + public static function decode(string $string = null): string + { + $string = strip_tags($string); + return html_entity_decode($string, ENT_COMPAT, 'utf-8'); + } + + /** + * Generates an `a` tag with `mailto:` + * + * @param string $email The url for the a tag + * @param mixed $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 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 (if the 'text' argument is empty) + $text = [Str::encode(Str::split($email, '?')[0])]; + } + + $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 a html-safe string + * + * @param string $string + * @param bool $keepTags + * @return string The html string + */ + public static function encode(string $string = null, bool $keepTags = false): string + { + 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_COMPAT, 'utf-8'); + } + + /** + * Returns the entities translation table + * + * @return array + */ + public static function entities(): array + { + return static::$entities = static::$entities ?? get_html_translation_table(HTML_ENTITIES); + } + + /** + * Creates a figure tag with optional caption + * + * @param string|array $content + * @param string|array $caption + * @param array $attr + * @return string + */ + public static function figure($content, $caption = null, 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 gist + * + * @param string $url + * @param string $file + * @param array $attr + * @return string + */ + public static function gist(string $url, string $file = null, array $attr = []): string + { + if ($file === null) { + $src = $url . '.js'; + } else { + $src = $url . '.js?file=' . $file; + } + + return static::tag('script', null, array_merge($attr, [ + 'src' => $src + ])); + } + + /** + * Creates an iframe + * + * @param string $src + * @param array $attr + * @return string + */ + public static function iframe(string $src, array $attr = []): string + { + return static::tag('iframe', null, array_merge(['src' => $src], $attr)); + } + + /** + * Generates an img tag + * + * @param string $src The url of the image + * @param array $attr Additional attributes for the image tag + * @return string the generated html + */ + public static function img(string $src, array $attr = []): string + { + $attr = array_merge([ + 'src' => $src, + 'alt' => ' ' + ], $attr); + + return static::tag('img', null, $attr); + } + + /** + * Checks if a tag is self-closing + * + * @param string $tag + * @return bool + */ + public static function isVoid(string $tag): bool + { + $void = [ + 'area', + 'base', + 'br', + 'col', + 'command', + 'embed', + 'hr', + 'img', + 'input', + 'keygen', + 'link', + 'meta', + 'param', + 'source', + 'track', + 'wbr', + ]; + + return in_array(strtolower($tag), $void); + } + + /** + * Generates an `a` link tag + * + * @param string $href The url for the `a` tag + * @param mixed $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 link(string $href = null, $text = null, array $attr = []): string + { + $attr = array_merge(['href' => $href], $attr); + + if (empty($text) === true) { + $text = $attr['href']; + } + + if (is_string($text) === true && Str::isUrl($text) === true) { + $text = Url::short($text); + } + + // 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); + } + + /** + * Add noopeener noreferrer to rels when target is `_blank` + * + * @param string $rel + * @param string $target + * @return string|null + */ + public static function rel(string $rel = null, string $target = null) + { + $rel = trim($rel); + + if ($target === '_blank') { + if (empty($rel) === false) { + return $rel; + } + + return trim($rel . ' noopener noreferrer', ' '); + } + + return $rel; + } + + /** + * Generates an Html tag with optional content and attributes + * + * @param string $name The name of the tag, i.e. `a` + * @param mixed $content The content if availble. Pass `null` to generate a self-closing tag, Pass an empty string to generate empty content + * @param array $attr An associative array with additional attributes for the tag + * @return string The generated Html + */ + public static function tag(string $name, $content = null, array $attr = []): string + { + $html = '<' . $name; + $attr = static::attr($attr); + + if (empty($attr) === false) { + $html .= ' ' . $attr; + } + + if (static::isVoid($name) === true) { + $html .= static::$void; + } else { + if (is_array($content) === true) { + $content = implode($content); + } else { + $content = static::encode($content, false); + } + + $html .= '>' . $content . ''; + } + + return $html; + } + + + /** + * Generates an `a` tag for a phone number + * + * @param string $tel The phone number + * @param mixed $text The optional text. If `null`, the number will be used as text + * @param array $attr Additional attributes for the tag + * @return string the generated html + */ + public static function tel($tel = null, $text = null, array $attr = []): string + { + $number = preg_replace('![^0-9\+]+!', '', $tel); + + if (empty($text) === true) { + $text = $tel; + } + + return static::link('tel:' . $number, $text, $attr); + } + + /** + * 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 + */ + public static function video(string $url, ?array $options = [], array $attr = []): string + { + // YouTube video + if (preg_match('!youtu!i', $url) === 1) { + return static::youtube($url, $options['youtube'] ?? [], $attr); + } + + // Vimeo video + if (preg_match('!vimeo!i', $url) === 1) { + return static::vimeo($url, $options['vimeo'] ?? [], $attr); + } + + throw new Exception('Unexpected video type'); + } + + /** + * Embeds a Vimeo video by URL in an iframe + * + * @param string $url + * @param array $options + * @param array $attr + * @return string + */ + public static function vimeo(string $url, ?array $options = [], array $attr = []): string + { + if (preg_match('!vimeo.com\/([0-9]+)!i', $url, $array) === 1) { + $id = $array[1]; + } elseif (preg_match('!player.vimeo.com\/video\/([0-9]+)!i', $url, $array) === 1) { + $id = $array[1]; + } else { + throw new Exception('Invalid Vimeo source'); + } + + // build the options query + if (empty($options) === false) { + $query = '?' . http_build_query($options); + } else { + $query = ''; + } + + $url = 'https://player.vimeo.com/video/' . $id . $query; + + return static::iframe($url, array_merge(['allowfullscreen' => true], $attr)); + } + + /** + * Embeds a Youtube video by URL in an iframe + * + * @param string $url + * @param array $options + * @param array $attr + * @return string + */ + public static function youtube(string $url, ?array $options = [], array $attr = []): string + { + // youtube embed domain + $domain = 'youtube.com'; + $id = null; + + $schemes = [ + // http://www.youtube.com/embed/d9NF2edxy-M + ['pattern' => 'youtube.com\/embed\/([a-zA-Z0-9_-]+)'], + // https://www.youtube-nocookie.com/embed/d9NF2edxy-M + [ + 'pattern' => 'youtube-nocookie.com\/embed\/([a-zA-Z0-9_-]+)', + 'domain' => 'www.youtube-nocookie.com' + ], + // https://www.youtube-nocookie.com/watch?v=d9NF2edxy-M + [ + 'pattern' => 'youtube-nocookie.com\/watch\?v=([a-zA-Z0-9_-]+)', + 'domain' => 'www.youtube-nocookie.com' + ], + // http://www.youtube.com/watch?v=d9NF2edxy-M + ['pattern' => 'v=([a-zA-Z0-9_-]+)'], + // http://youtu.be/d9NF2edxy-M + ['pattern' => 'youtu.be\/([a-zA-Z0-9_-]+)'] + ]; + + foreach ($schemes as $schema) { + if (preg_match('!' . $schema['pattern'] . '!i', $url, $array) === 1) { + $domain = $schema['domain'] ?? $domain; + $id = $array[1]; + break; + } + } + + // no match + if ($id === null) { + throw new Exception('Invalid Youtube source'); + } + + // build the options query + if (empty($options) === false) { + $query = '?' . http_build_query($options); + } else { + $query = ''; + } + + $url = 'https://' . $domain . '/embed/' . $id . $query; + + return static::iframe($url, array_merge(['allowfullscreen' => true], $attr)); + } +} diff --git a/kirby/src/Toolkit/I18n.php b/kirby/src/Toolkit/I18n.php new file mode 100755 index 0000000..8e44cdf --- /dev/null +++ b/kirby/src/Toolkit/I18n.php @@ -0,0 +1,230 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class I18n +{ + /** + * Custom loader function + * + * @var Closure + */ + public static $load = null; + + /** + * Current locale + * + * @var string + */ + public static $locale = 'en'; + + /** + * All registered translations + * + * @var array + */ + public static $translations = []; + + /** + * The fallback locale + * + * @var string + */ + public static $fallback = 'en'; + + /** + * Returns the fallback code + * + * @return string + */ + public static function fallback(): string + { + if (is_string(static::$fallback) === true) { + return static::$fallback; + } + + if (is_callable(static::$fallback) === true) { + return static::$fallback = (static::$fallback)(); + } + + return static::$fallback = 'en'; + } + + /** + * Returns singular or plural + * depending on the given number + * + * @param int $count + * @param bool $none If true, 'none' will be returned if the count is 0 + * @return string + */ + public static function form(int $count, bool $none = false): string + { + if ($none === true && $count === 0) { + return 'none'; + } + + return $count === 1 ? 'singular' : 'plural'; + } + + /** + * Returns the locale code + * + * @return string + */ + public static function locale(): string + { + if (is_string(static::$locale) === true) { + return static::$locale; + } + + if (is_callable(static::$locale) === true) { + return static::$locale = (static::$locale)(); + } + + return static::$locale = 'en'; + } + + /** + * Translates a given message + * according to the currently set locale + * + * @param string|array $key + * @param string|array|null $fallback + * @param string|null $locale + * @return string|array|null + */ + public static function translate($key, $fallback = null, string $locale = null) + { + $locale = $locale ?? static::locale(); + + if (is_array($key) === true) { + if (isset($key[$locale])) { + return $key[$locale]; + } + if (is_array($fallback)) { + return $fallback[$locale] ?? $fallback['en'] ?? reset($fallback); + } + return $fallback; + } + + if ($translation = static::translation($locale)[$key] ?? null) { + return $translation; + } + + if ($fallback !== null) { + return $fallback; + } + + if ($locale !== static::fallback()) { + return static::translation(static::fallback())[$key] ?? null; + } + + return null; + } + + /** + * Translate by key and then replace + * placeholders in the text + * + * @param string $key + * @param string $fallback + * @param array $replace + * @param string $locale + * @return string + */ + public static function template(string $key, $fallback = null, array $replace = null, string $locale = null) + { + if (is_array($fallback) === true) { + $replace = $fallback; + $fallback = null; + $locale = null; + } + + $template = static::translate($key, $fallback, $locale); + return Str::template($template, $replace, '-', '{', '}'); + } + + /** + * Returns the current or any other translation + * by locale. If the translation does not exist + * yet, the loader will try to load it, if defined. + * + * @param string|null $locale + * @return array + */ + public static function translation(string $locale = null): array + { + $locale = $locale ?? static::locale(); + + if (isset(static::$translations[$locale]) === true) { + return static::$translations[$locale]; + } + + if (is_a(static::$load, 'Closure') === true) { + return static::$translations[$locale] = (static::$load)($locale); + } + + return static::$translations[$locale] = []; + } + + /** + * Returns all loaded or defined translations + * + * @return array + */ + public static function translations(): array + { + return static::$translations; + } + + /** + * Translate amounts + * + * @param string $key + * @param int $count + * @param string $locale + * @return mixed + */ + public static function translateCount(string $key, int $count, string $locale = null) + { + $translation = static::translate($key, null, $locale); + + if ($translation === null) { + return null; + } + + if (is_string($translation) === true) { + return $translation; + } + + if (count($translation) !== 3) { + throw new Exception('Please provide 3 translations'); + } + + switch ($count) { + case 0: + $message = $translation[0]; + break; + case 1: + $message = $translation[1]; + break; + default: + $message = $translation[2]; + } + + return str_replace('{{ count }}', $count, $message); + } +} diff --git a/kirby/src/Toolkit/Iterator.php b/kirby/src/Toolkit/Iterator.php new file mode 100755 index 0000000..81fb2ae --- /dev/null +++ b/kirby/src/Toolkit/Iterator.php @@ -0,0 +1,181 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Iterator implements IteratorAggregate +{ + /** + * The data array + * + * @var array + */ + public $data = []; + + /** + * Constructor + * + * @param array $data + */ + public function __construct(array $data = []) + { + $this->data = $data; + } + + /** + * Get an iterator for the items. + * + * @return \ArrayIterator + */ + public function getIterator() + { + return new ArrayIterator($this->data); + } + + /** + * Returns the current key + * + * @return string + */ + public function key() + { + return key($this->data); + } + + /** + * Returns an array of all keys + * + * @return array + */ + public function keys(): array + { + return array_keys($this->data); + } + + /** + * Returns the current element + * + * @return mixed + */ + public function current() + { + return current($this->data); + } + + /** + * Moves the cursor to the previous element + * and returns it + * + * @return mixed + */ + public function prev() + { + return prev($this->data); + } + + /** + * Moves the cursor to the next element + * and returns it + * + * @return mixed + */ + public function next() + { + return next($this->data); + } + + /** + * Moves the cusor to the first element + */ + public function rewind() + { + reset($this->data); + } + + /** + * Checks if the current element is valid + * + * @return bool + */ + public function valid(): bool + { + return $this->current() !== false; + } + + /** + * Counts all elements + * + * @return int + */ + public function count(): int + { + return count($this->data); + } + + /** + * Tries to find the index number for the given element + * + * @param mixed $needle the element to search for + * @return string|false the name of the key or false + */ + public function indexOf($needle) + { + return array_search($needle, array_values($this->data)); + } + + /** + * Tries to find the key for the given element + * + * @param mixed $needle the element to search for + * @return string|false the name of the key or false + */ + public function keyOf($needle) + { + return array_search($needle, $this->data); + } + + /** + * Checks by key if an element is included + * + * @param mixed $key + * @return bool + */ + public function has($key): bool + { + return isset($this->data[$key]); + } + + /** + * Checks if the current key is set + * + * @param mixed $key the key to check + * @return bool + */ + public function __isset($key): bool + { + return $this->has($key); + } + + /** + * Simplified var_dump output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->data; + } +} diff --git a/kirby/src/Toolkit/Mime.php b/kirby/src/Toolkit/Mime.php new file mode 100755 index 0000000..52ce12b --- /dev/null +++ b/kirby/src/Toolkit/Mime.php @@ -0,0 +1,340 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +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', + '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/x-javascript', + 'json' => ['application/json', 'text/json'], + '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'], + ]; + + /** + * 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\Toolkit\Mime', 'fromSvg'], + ], + 'text/plain' => [ + 'css' => 'text/css', + 'json' => 'application/json', + 'svg' => ['Kirby\Toolkit\Mime', 'fromSvg'], + ], + 'text/x-asm' => [ + 'css' => 'text/css' + ], + 'image/svg' => [ + 'svg' => 'image/svg+xml' + ] + ]; + + if ($mode = ($map[$mime][$extension] ?? null)) { + if (is_callable($mode) === true) { + return $mode($file, $mime, $extension); + } + + if (is_string($mode) === true) { + return $mode; + } + } + + return $mime; + } + + /** + * Guesses a MIME type by extension + * + * @param string $extension + * @return string|null + */ + public static function fromExtension(string $extension) + { + $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; + } + + 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); + } + + 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); + + $svg = new SimpleXMLElement(file_get_contents($file)); + + if ($svg !== false && $svg->getName() === 'svg') { + return 'image/svg+xml'; + } + } + + 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); + + foreach ($accepted as $m) { + if (static::matches($mime, $m['value']) === true) { + return true; + } + } + + 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; + } + + /** + * 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; + } + } + + 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 = []; + + foreach (static::$types as $key => $value) { + if (is_array($value) === true && in_array($mime, $value) === true) { + $extensions[] = $key; + continue; + } + + if ($value === $mime) { + $extensions[] = $key; + continue; + } + } + + 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); + + // use the mime_content_type function + if ($mime === false) { + $mime = static::fromMimeContentType($file); + } + + // get the extension or extract it from the filename + $extension = $extension ?? F::extension($file); + + // 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); + } + + /** + * Returns all detectable MIME types + * + * @return array + */ + public static function types(): array + { + return static::$types; + } +} diff --git a/kirby/src/Toolkit/Obj.php b/kirby/src/Toolkit/Obj.php new file mode 100755 index 0000000..bce4e9b --- /dev/null +++ b/kirby/src/Toolkit/Obj.php @@ -0,0 +1,106 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Obj extends stdClass +{ + /** + * Constructor + * + * @param array $data + */ + public function __construct(array $data = []) + { + foreach ($data as $key => $val) { + $this->$key = $val; + } + } + + /** + * Magic getter + * + * @param string $property + * @param array $arguments + * @return mixed + */ + public function __call(string $property, array $arguments) + { + return $this->$property ?? null; + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } + + /** + * Magic property getter + * + * @param string $property + * @return mixed + */ + public function __get(string $property) + { + return null; + } + + /** + * Property Getter + * + * @param string $property + * @param mixed $fallback + * @return mixed + */ + public function get(string $property, $fallback = null) + { + return $this->$property ?? $fallback; + } + + /** + * Converts the object to an array + * + * @return array + */ + public function toArray(): array + { + $result = []; + + foreach ((array)$this as $key => $value) { + if (is_object($value) === true && method_exists($value, 'toArray')) { + $result[$key] = $value->toArray(); + } else { + $result[$key] = $value; + } + } + + return $result; + } + + /** + * Converts the object to a json string + * + * @param mixed ...$arguments + * @return string + */ + public function toJson(...$arguments): string + { + return json_encode($this->toArray(), ...$arguments); + } +} diff --git a/kirby/src/Toolkit/Pagination.php b/kirby/src/Toolkit/Pagination.php new file mode 100755 index 0000000..bcb47f5 --- /dev/null +++ b/kirby/src/Toolkit/Pagination.php @@ -0,0 +1,502 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Pagination +{ + use Properties { + setProperties as protected baseSetProperties; + } + + /** + * The current page + * + * @var int + */ + protected $page; + + /** + * Total number of items + * + * @var int + */ + protected $total = 0; + + /** + * The number of items per page + * + * @var int + */ + protected $limit = 20; + + /** + * Whether validation of the pagination page + * is enabled; will throw Exceptions if true + * + * @var bool + */ + public static $validate = true; + + /** + * Creates a new pagination object + * with the given parameters + * + * @param array $props + */ + public function __construct(array $props = []) + { + $this->setProperties($props); + } + + /** + * Creates a pagination instance for the given + * collection with a flexible argument api + * + * @param \Kirby\Toolkit\Collection $collection + * @param mixed ...$arguments + * @return self + */ + public static function for(Collection $collection, ...$arguments) + { + $a = $arguments[0] ?? null; + $b = $arguments[1] ?? null; + + $params = []; + + if (is_array($a) === true) { + + /** + * First argument is an option array + * + * $collection->paginate([...]) + */ + $params = $a; + } elseif (is_int($a) === true && $b === null) { + + /** + * First argument is the limit + * + * $collection->paginate(10) + */ + $params['limit'] = $a; + } elseif (is_int($a) === true && is_int($b) === true) { + + /** + * First argument is the limit, + * second argument is the page + * + * $collection->paginate(10, 2) + */ + $params['limit'] = $a; + $params['page'] = $b; + } elseif (is_int($a) === true && is_array($b) === true) { + + /** + * First argument is the limit, + * second argument are options + * + * $collection->paginate(10, [...]) + */ + $params = $b; + $params['limit'] = $a; + } + + // add the total count from the collection + $params['total'] = $collection->count(); + + // remove null values to make later merges work properly + $params = array_filter($params); + + // create the pagination instance + return new static($params); + } + + /** + * Getter for the current page + * + * @deprecated 3.3.0 Setter is no longer supported, use $pagination->clone() + * @return int + */ + public function page(int $page = null): int + { + if ($page !== null) { + throw new Exception('$pagination->page() setter is no longer supported, use $pagination->clone()'); // @codeCoverageIgnore + } + + return $this->page; + } + + /** + * Getter for the total number of items + * + * @deprecated 3.3.0 Setter is no longer supported, use $pagination->clone() + * @return int + */ + public function total(int $total = null): int + { + if ($total !== null) { + throw new Exception('$pagination->total() setter is no longer supported, use $pagination->clone()'); // @codeCoverageIgnore + } + + return $this->total; + } + + /** + * Getter for the number of items per page + * + * @deprecated 3.3.0 Setter is no longer supported, use $pagination->clone() + * @return int + */ + public function limit(int $limit = null): int + { + if ($limit !== null) { + throw new Exception('$pagination->limit() setter is no longer supported, use $pagination->clone()'); // @codeCoverageIgnore + } + + return $this->limit; + } + + /** + * Returns the index of the first item on the page + * + * @return int + */ + public function start(): int + { + $index = $this->page() - 1; + + if ($index < 0) { + $index = 0; + } + + return $index * $this->limit() + 1; + } + + /** + * Returns the index of the last item on the page + * + * @return int + */ + public function end(): int + { + $value = ($this->start() - 1) + $this->limit(); + + if ($value <= $this->total()) { + return $value; + } + + return $this->total(); + } + + /** + * Returns the total number of pages + * + * @return int + */ + public function pages(): int + { + if ($this->total() === 0) { + return 0; + } + + return ceil($this->total() / $this->limit()); + } + + /** + * Returns the first page + * + * @return int + */ + public function firstPage(): int + { + return $this->total() === 0 ? 0 : 1; + } + + /** + * Returns the last page + * + * @return int + */ + public function lastPage(): int + { + return $this->pages(); + } + + /** + * Returns the offset (i.e. for db queries) + * + * @return int + */ + public function offset(): int + { + return $this->start() - 1; + } + + /** + * Checks if the given page exists + * + * @param int $page + * @return bool + */ + public function hasPage(int $page): bool + { + if ($page <= 0) { + return false; + } + + if ($page > $this->pages()) { + return false; + } + + return true; + } + + /** + * Checks if there are any pages at all + * + * @return bool + */ + public function hasPages(): bool + { + return $this->total() > $this->limit(); + } + + /** + * Checks if there's a previous page + * + * @return bool + */ + public function hasPrevPage(): bool + { + return $this->page() > 1; + } + + /** + * Returns the previous page + * + * @return int|null + */ + public function prevPage() + { + return $this->hasPrevPage() ? $this->page() - 1 : null; + } + + /** + * Checks if there's a next page + * + * @return bool + */ + public function hasNextPage(): bool + { + return $this->end() < $this->total(); + } + + /** + * Returns the next page + * + * @return int|null + */ + public function nextPage() + { + return $this->hasNextPage() ? $this->page() + 1 : null; + } + + /** + * Checks if the current page is the first page + * + * @return bool + */ + public function isFirstPage(): bool + { + return $this->page() === $this->firstPage(); + } + + /** + * Checks if the current page is the last page + * + * @return bool + */ + public function isLastPage(): bool + { + return $this->page() === $this->lastPage(); + } + + /** + * Creates a range of page numbers for Google-like pagination + * + * @param int $range + * @return array + */ + public function range(int $range = 5): array + { + $page = $this->page(); + $pages = $this->pages(); + $start = 1; + $end = $pages; + + if ($pages <= $range) { + return range($start, $end); + } + + $start = $page - (int)floor($range/2); + $end = $page + (int)floor($range/2); + + if ($start <= 0) { + $end += abs($start); + $start = 1; + } + + if ($end > $pages) { + $start -= $end - $pages; + $end = $pages; + } + + return range($start, $end); + } + + /** + * Returns the first page of the created range + * + * @param int $range + * @return int + */ + public function rangeStart(int $range = 5): int + { + return $this->range($range)[0]; + } + + /** + * Returns the last page of the created range + * + * @param int $range + * @return int + */ + public function rangeEnd(int $range = 5): int + { + $range = $this->range($range); + return array_pop($range); + } + + /** + * Sets the properties limit, total and page + * and validates that the properties match + * + * @param array $props Array with keys limit, total and/or page + * @return self + */ + protected function setProperties(array $props) + { + $this->baseSetProperties($props); + + // ensure that page is set to something, otherwise + // generate "default page" based on other params + if ($this->page === null) { + $this->page = $this->firstPage(); + } + + // allow a page value of 1 even if there are no pages; + // otherwise the exception will get thrown for this pretty common case + $min = $this->firstPage(); + $max = $this->pages(); + if ($this->page === 1 && $max === 0) { + $this->page = 0; + } + + // validate page based on all params if validation is enabled, + // otherwise limit the page number to the bounds + if ($this->page < $min || $this->page > $max) { + if (static::$validate === true) { + throw new ErrorPageException('Pagination page ' . $this->page . ' does not exist, expected ' . $min . '-' . $max); + } else { + $this->page = max(min($this->page, $max), $min); + } + } + + return $this; + } + + /** + * Sets the number of items per page + * + * @param int $limit + * @return self + */ + protected function setLimit(int $limit = 20) + { + if ($limit < 1) { + throw new Exception('Invalid pagination limit: ' . $limit); + } + + $this->limit = $limit; + return $this; + } + + /** + * Sets the total number of items + * + * @param int $total + * @return self + */ + protected function setTotal(int $total = 0) + { + if ($total < 0) { + throw new Exception('Invalid total number of items: ' . $total); + } + + $this->total = $total; + return $this; + } + + /** + * Sets the current page + * + * @param int|string|null $page Int or int in string form; + * automatically determined if null + * @return self + */ + protected function setPage($page = null) + { + // if $page is null, it is set to a default in the setProperties() method + if ($page !== null) { + if (is_numeric($page) !== true || $page < 0) { + throw new Exception('Invalid page number: ' . $page); + } + + $this->page = (int)$page; + } + + return $this; + } + + /** + * Returns an array with all properties + * + * @return array + */ + public function toArray(): array + { + return [ + 'page' => $this->page(), + 'firstPage' => $this->firstPage(), + 'lastPage' => $this->lastPage(), + 'pages' => $this->pages(), + 'offset' => $this->offset(), + 'limit' => $this->limit(), + 'total' => $this->total(), + 'start' => $this->start(), + 'end' => $this->end(), + ]; + } +} diff --git a/kirby/src/Toolkit/Properties.php b/kirby/src/Toolkit/Properties.php new file mode 100755 index 0000000..52c356b --- /dev/null +++ b/kirby/src/Toolkit/Properties.php @@ -0,0 +1,151 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +trait Properties +{ + protected $propertyData = []; + + /** + * Creates an instance with the same + * initial properties. + * + * @param array $props + * @return self + */ + public function clone(array $props = []) + { + return new static(array_replace_recursive($this->propertyData, $props)); + } + + /** + * Creates a clone and fetches all + * lazy-loaded getters to get a full copy + * + * @return self + */ + public function hardcopy() + { + $clone = $this->clone(); + $clone->propertiesToArray(); + return $clone; + } + + protected function isRequiredProperty(string $name): bool + { + $method = new ReflectionMethod($this, 'set' . $name); + return $method->getNumberOfRequiredParameters() > 0; + } + + protected function propertiesToArray() + { + $array = []; + + foreach (get_object_vars($this) as $name => $default) { + if ($name === 'propertyData') { + continue; + } + + if (method_exists($this, 'convert' . $name . 'ToArray') === true) { + $array[$name] = $this->{'convert' . $name . 'ToArray'}(); + continue; + } + + if (method_exists($this, $name) === true) { + $method = new ReflectionMethod($this, $name); + + if ($method->isPublic() === true) { + $value = $this->$name(); + + if (is_object($value) === false) { + $array[$name] = $value; + } + } + } + } + + ksort($array); + + return $array; + } + + protected function setOptionalProperties(array $props, array $optional) + { + $this->propertyData = array_merge($this->propertyData, $props); + + foreach ($optional as $propertyName) { + if (isset($props[$propertyName]) === true) { + $this->{'set' . $propertyName}($props[$propertyName]); + } else { + $this->{'set' . $propertyName}(); + } + } + } + + protected function setProperties($props, array $keys = null) + { + foreach (get_object_vars($this) as $name => $default) { + if ($name === 'propertyData') { + continue; + } + + $this->setProperty($name, $props[$name] ?? $default); + } + + return $this; + } + + protected function setProperty($name, $value, $required = null) + { + // use a setter if it exists + if (method_exists($this, 'set' . $name) === false) { + return $this; + } + + // fetch the default value from the property + $value = $value ?? $this->$name ?? null; + + // store all original properties, to be able to clone them later + $this->propertyData[$name] = $value; + + // handle empty values + if ($value === null) { + + // replace null with a default value, if a default handler exists + if (method_exists($this, 'default' . $name) === true) { + $value = $this->{'default' . $name}(); + } + + // check for required properties + if ($value === null && ($required ?? $this->isRequiredProperty($name)) === true) { + throw new Exception(sprintf('The property "%s" is required', $name)); + } + } + + // call the setter with the final value + return $this->{'set' . $name}($value); + } + + protected function setRequiredProperties(array $props, array $required) + { + foreach ($required as $propertyName) { + if (isset($props[$propertyName]) !== true) { + throw new Exception(sprintf('The property "%s" is required', $propertyName)); + } + + $this->{'set' . $propertyName}($props[$propertyName]); + } + } +} diff --git a/kirby/src/Toolkit/Query.php b/kirby/src/Toolkit/Query.php new file mode 100755 index 0000000..391f0f2 --- /dev/null +++ b/kirby/src/Toolkit/Query.php @@ -0,0 +1,203 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Query +{ + const PARTS = '!([a-zA-Z_]*(\(.*?\))?)\.|' . self::SKIP . '!'; + const METHOD = '!\((.*)\)!'; + const PARAMETERS = '!,|' . self::SKIP . '!'; + + const NO_PNTH = '\([^\(]+\)(*SKIP)(*FAIL)'; + const NO_SQBR = '\[[^]]+\](*SKIP)(*FAIL)'; + const NO_DLQU = '\"[^"]+\"(*SKIP)(*FAIL)'; + const NO_SLQU = '\'[^\']+\'(*SKIP)(*FAIL)'; + const SKIP = self::NO_PNTH . '|' . self::NO_SQBR . '|' . + self::NO_DLQU . '|' . self::NO_SLQU; + + /** + * The query string + * + * @var string + */ + protected $query; + + /** + * Queryable data + * + * @var array + */ + protected $data; + + /** + * Creates a new Query object + * + * @param string $query + * @param array|object $data + */ + public function __construct(string $query = null, $data = []) + { + $this->query = $query; + $this->data = $data; + } + + /** + * Returns the query result if anything + * can be found. Otherwise returns null. + * + * @return mixed + */ + public function result() + { + if (empty($this->query) === true) { + return $this->data; + } + + return $this->resolve($this->query); + } + + /** + * Resolves the query if anything + * can be found. Otherwise returns null. + * + * @param string $query + * @return mixed + */ + protected function resolve(string $query) + { + // direct key access in arrays + if (is_array($this->data) === true && array_key_exists($query, $this->data) === true) { + return $this->data[$query]; + } + + $parts = $this->parts($query); + $data = $this->data; + $value = null; + + while (count($parts)) { + $part = array_shift($parts); + $info = $this->part($part); + $method = $info['method']; + $value = null; + + if (is_array($data)) { + $value = $data[$method] ?? null; + } elseif (is_object($data)) { + if (method_exists($data, $method) || method_exists($data, '__call')) { + $value = $data->$method(...$info['args']); + } + } elseif (is_scalar($data)) { + return $data; + } else { + return null; + } + + if (is_array($value) || is_object($value)) { + $data = $value; + } + } + + return $value; + } + + /** + * Breaks the query string down into its components + * + * @param string $query + * @return array + */ + protected function parts(string $query): array + { + $query = trim($query); + + // match all parts but the last + preg_match_all(self::PARTS, $query, $match); + + // remove all matched parts from the query to retrieve last part + foreach ($match[0] as $part) { + $query = Str::after($query, $part); + } + + array_push($match[1], $query); + return $match[1]; + } + + /** + * Analyzes each part of the query string and + * extracts methods and method arguments. + * + * @param string $part + * @return array + */ + protected function part(string $part): array + { + $args = []; + $method = preg_replace_callback(self::METHOD, function ($match) use (&$args) { + $args = preg_split(self::PARAMETERS, $match[1]); + $args = array_map('self::parameter', $args); + }, $part); + + return [ + 'method' => $method, + 'args' => $args + ]; + } + + /** + * Converts a parameter of query to + * proper type. + * + * @param mixed $arg + * @return mixed + */ + protected function parameter($arg) + { + $arg = trim($arg); + + // string with double quotes + if (substr($arg, 0, 1) === '"') { + return trim($arg, '"'); + } + + // string with single quotes + if (substr($arg, 0, 1) === '\'') { + return trim($arg, '\''); + } + + // boolean or null + switch ($arg) { + case 'null': + return null; + case 'false': + return false; + case 'true': + return true; + } + + // numeric + if (is_numeric($arg) === true) { + return (float)$arg; + } + + // array: split and recursive sanitizing + if (substr($arg, 0, 1) === '[' && substr($arg, -1) === ']') { + $arg = substr($arg, 1, -1); + $arg = preg_split(self::PARAMETERS, $arg); + return array_map('self::parameter', $arg); + } + + // resolve parameter for objects and methods itself + return $this->resolve($arg); + } +} diff --git a/kirby/src/Toolkit/Silo.php b/kirby/src/Toolkit/Silo.php new file mode 100755 index 0000000..4158ea6 --- /dev/null +++ b/kirby/src/Toolkit/Silo.php @@ -0,0 +1,73 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Silo +{ + /** + * @var array + */ + public static $data = []; + + /** + * Setter for new data. + * + * @param string|array $key + * @param mixed $value + * @return array + */ + public static function set($key, $value = null): array + { + if (is_array($key) === true) { + return static::$data = array_merge(static::$data, $key); + } else { + static::$data[$key] = $value; + return static::$data; + } + } + + /** + * @param string|array $key + * @param mixed $default + * @return mixed + */ + public static function get($key = null, $default = null) + { + if ($key === null) { + return static::$data; + } + + return A::get(static::$data, $key, $default); + } + + /** + * Removes an item from the data array + * + * @param string|null $key + * @return array + */ + public static function remove(string $key = null): array + { + // reset the entire array + if ($key === null) { + return static::$data = []; + } + + // unset a single key + unset(static::$data[$key]); + + // return the array without the removed key + return static::$data; + } +} diff --git a/kirby/src/Toolkit/Str.php b/kirby/src/Toolkit/Str.php new file mode 100755 index 0000000..8ca6421 --- /dev/null +++ b/kirby/src/Toolkit/Str.php @@ -0,0 +1,1059 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Str +{ + /** + * Language translation table + * + * @var array + */ + public static $language = []; + + /** + * Ascii translation table + * + * @var array + */ + public static $ascii = [ + '/°|₀/' => '0', + '/¹|₁/' => '1', + '/²|₂/' => '2', + '/³|₃/' => '3', + '/⁴|₄/' => '4', + '/⁵|₅/' => '5', + '/⁶|₆/' => '6', + '/⁷|₇/' => '7', + '/⁸|₈/' => '8', + '/⁹|₉/' => '9', + '/À|Á|Â|Ã|Å|Ǻ|Ā|Ă|Ą|Ǎ|Ä|A/' => 'A', + '/à|á|â|ã|å|ǻ|ā|ă|ą|ǎ|ª|æ|ǽ|ä|a|а/' => 'a', + '/Б/' => 'B', + '/б/' => 'b', + '/Ç|Ć|Ĉ|Ċ|Č|Ц/' => 'C', + '/ç|ć|ĉ|ċ|č|ц/' => 'c', + '/Ð|Ď|Đ/' => 'Dj', + '/ð|ď|đ/' => 'dj', + '/Д/' => 'D', + '/д/' => 'd', + '/È|É|Ê|Ë|Ē|Ĕ|Ė|Ę|Ě|Е|Ё|Э/' => 'E', + '/è|é|ê|ë|ē|ĕ|ė|ę|ě|е|ё|э/' => 'e', + '/Ф/' => 'F', + '/ƒ|ф/' => 'f', + '/Ĝ|Ğ|Ġ|Ģ|Г/' => 'G', + '/ĝ|ğ|ġ|ģ|г/' => 'g', + '/Ĥ|Ħ|Х/' => 'H', + '/ĥ|ħ|х/' => 'h', + '/Ì|Í|Î|Ï|Ĩ|Ī|Ĭ|Ǐ|Į|İ|И/' => 'I', + '/ì|í|î|ï|ĩ|ī|ĭ|ǐ|į|ı|и|i̇/' => 'i', + '/Ĵ|Й/' => 'J', + '/ĵ|й/' => 'j', + '/Ķ|К/' => 'K', + '/ķ|к/' => 'k', + '/Ĺ|Ļ|Ľ|Ŀ|Ł|Л/' => 'L', + '/ĺ|ļ|ľ|ŀ|ł|л/' => 'l', + '/М/' => 'M', + '/м/' => 'm', + '/Ñ|Ń|Ņ|Ň|Н/' => 'N', + '/ñ|ń|ņ|ň|ʼn|н/' => 'n', + '/Ò|Ó|Ô|Õ|Ō|Ŏ|Ǒ|Ő|Ơ|Ø|Ǿ|Ö|O/' => 'O', + '/ò|ó|ô|õ|ō|ŏ|ǒ|ő|ơ|ø|ǿ|º|ö|o|о/' => 'o', + '/П/' => 'P', + '/п/' => 'p', + '/Ŕ|Ŗ|Ř|Р/' => 'R', + '/ŕ|ŗ|ř|р/' => 'r', + '/Ś|Ŝ|Ş|Ș|Š|С/' => 'S', + '/ś|ŝ|ş|ș|š|ſ|с/' => 's', + '/Ţ|Ț|Ť|Ŧ|Т/' => 'T', + '/ţ|ț|ť|ŧ|т/' => 't', + '/Ù|Ú|Û|Ũ|Ū|Ŭ|Ů|Ű|Ų|Ư|Ǔ|Ǖ|Ǘ|Ǚ|Ǜ|У|Ü|U/' => 'U', + '/ù|ú|û|ũ|ū|ŭ|ů|ű|ų|ư|ǔ|ǖ|ǘ|ǚ|ǜ|у|ü|u/' => 'u', + '/В/' => 'V', + '/в/' => 'v', + '/Ý|Ÿ|Ŷ|Ы/' => 'Y', + '/ý|ÿ|ŷ|ы/' => 'y', + '/Ŵ/' => 'W', + '/ŵ/' => 'w', + '/Ź|Ż|Ž|З/' => 'Z', + '/ź|ż|ž|з/' => 'z', + '/Æ|Ǽ/' => 'AE', + '/ß/' => 'ss', + '/IJ/' => 'IJ', + '/ij/' => 'ij', + '/Œ/' => 'OE', + '/Ч/' => 'Ch', + '/ч/' => 'ch', + '/Ю/' => 'Ju', + '/ю/' => 'ju', + '/Я/' => 'Ja', + '/я/' => 'ja', + '/Ш/' => 'Sh', + '/ш/' => 'sh', + '/Щ/' => 'Shch', + '/щ/' => 'shch', + '/Ж/' => 'Zh', + '/ж/' => 'zh', + ]; + + /** + * Default settings for class methods + * + * @var array + */ + public static $defaults = [ + 'slug' => [ + 'separator' => '-', + 'allowed' => 'a-z0-9' + ] + ]; + + /** + * Parse accepted values and their quality from an + * accept string like an Accept or Accept-Language header + * + * @param string $input + * @return array + */ + public static function accepted(string $input): array + { + $items = []; + + // check each type in the Accept header + foreach (static::split($input, ',') as $item) { + $parts = static::split($item, ';'); + $value = A::first($parts); // $parts now only contains params + $quality = 1; + + // check for the q param ("quality" of the type) + foreach ($parts as $param) { + $param = static::split($param, '='); + if (A::get($param, 0) === 'q' && !empty($param[1])) { + $quality = $param[1]; + } + } + + $items[$quality][] = $value; + } + + // sort items by quality + krsort($items); + + $result = []; + + foreach ($items as $quality => $values) { + foreach ($values as $value) { + $result[] = [ + 'quality' => $quality, + 'value' => $value + ]; + } + } + + return $result; + } + + /** + * Returns the rest of the string after the given character + * + * @param string $string + * @param string $needle + * @param bool $caseInsensitive + * @return string + */ + public static function after(string $string, string $needle, bool $caseInsensitive = false): string + { + $position = static::position($string, $needle, $caseInsensitive); + + if ($position === false) { + return false; + } else { + return static::substr($string, $position + static::length($needle)); + } + } + + /** + * Convert a string to 7-bit ASCII. + * + * @param string $string + * @return string + */ + public static function ascii(string $string): string + { + $string = str_replace( + array_keys(static::$language), + array_values(static::$language), + $string + ); + + $string = preg_replace( + array_keys(static::$ascii), + array_values(static::$ascii), + $string + ); + + return preg_replace('/[^\x09\x0A\x0D\x20-\x7E]/', '', $string); + } + + /** + * Returns the beginning of a string before the given character + * + * @param string $string + * @param string $needle + * @param bool $caseInsensitive + * @return string + */ + public static function before(string $string, string $needle, bool $caseInsensitive = false): string + { + $position = static::position($string, $needle, $caseInsensitive); + + if ($position === false) { + return false; + } else { + return static::substr($string, 0, $position); + } + } + + /** + * Returns everything between two strings from the first occurrence of a given string + * + * @param string $string + * @param string $start + * @param string $end + * @return string + */ + public static function between(string $string = null, string $start, string $end): string + { + return static::before(static::after($string, $start), $end); + } + + /** + * Checks if a str contains another string + * + * @param string $string + * @param string $needle + * @param bool $caseInsensitive + * @return bool + */ + public static function contains(string $string = null, string $needle, bool $caseInsensitive = false): bool + { + return call_user_func($caseInsensitive === true ? 'stristr' : 'strstr', $string, $needle) !== false; + } + + /** + * Converts a string to a different encoding + * + * @param string $string + * @param string $targetEncoding + * @param string $sourceEncoding (optional) + * @return string + */ + public static function convert($string, $targetEncoding, $sourceEncoding = null) + { + // detect the source encoding if not passed as third argument + if ($sourceEncoding === null) { + $sourceEncoding = static::encoding($string); + } + + // no need to convert if the target encoding is the same + if (strtolower($sourceEncoding) === strtolower($targetEncoding)) { + return $string; + } + + return iconv($sourceEncoding, $targetEncoding, $string); + } + + /** + * Encode a string (used for email addresses) + * + * @param string $string + * @return string + */ + public static function encode(string $string): string + { + $encoded = ''; + + for ($i = 0; $i < static::length($string); $i++) { + $char = static::substr($string, $i, 1); + list(, $code) = unpack('N', mb_convert_encoding($char, 'UCS-4BE', 'UTF-8')); + $encoded .= rand(1, 2) == 1 ? '&#' . $code . ';' : '&#x' . dechex($code) . ';'; + } + + return $encoded; + } + + /** + * Tries to detect the string encoding + * + * @param string $string + * @return string + */ + public static function encoding(string $string): string + { + return mb_detect_encoding($string, 'UTF-8, ISO-8859-1, windows-1251', true); + } + + /** + * Checks if a string ends with the passed needle + * + * @param string $string + * @param string $needle + * @param bool $caseInsensitive + * @return bool + */ + public static function endsWith(string $string, string $needle, bool $caseInsensitive = false): bool + { + if ($needle === '') { + return true; + } + + $probe = static::substr($string, -static::length($needle)); + + if ($caseInsensitive === true) { + $needle = static::lower($needle); + $probe = static::lower($probe); + } + + return $needle === $probe; + } + + /** + * Creates an excerpt of a string + * It removes all html tags first and then cuts the string + * according to the specified number of chars. + * + * @param string $string The string to be shortened + * @param int $chars The final number of characters the string should have + * @param bool $strip True: remove the HTML tags from the string first + * @param string $rep The element, which should be added if the string is too long. Ellipsis is the default. + * @return string The shortened string + */ + public static function excerpt($string, $chars = 140, $strip = true, $rep = '…') + { + if ($strip === true) { + $string = strip_tags(str_replace('<', ' <', $string)); + } + + // replace line breaks with spaces + $string = str_replace(PHP_EOL, ' ', trim($string)); + + // remove double spaces + $string = preg_replace('![ ]{2,}!', ' ', $string); + + if ($chars === 0) { + return $string; + } + + if (static::length($string) <= $chars) { + return $string; + } + + return static::substr($string, 0, strrpos(static::substr($string, 0, $chars), ' ')) . ' ' . $rep; + } + + /** + * Convert the value to a float with a decimal + * point, no matter what the locale setting is + * + * @param string|int|float $value + * @return string + */ + public static function float($value): string + { + $value = str_replace(',', '.', $value); + $decimal = strlen(substr(strrchr($value, '.'), 1)); + return number_format((float)$value, $decimal, '.', false); + } + + /** + * Returns the rest of the string starting from the given character + * + * @param string $string + * @param string $needle + * @param bool $caseInsensitive + * @return string + */ + public static function from(string $string, string $needle, bool $caseInsensitive = false): string + { + $position = static::position($string, $needle, $caseInsensitive); + + if ($position === false) { + return false; + } else { + return static::substr($string, $position); + } + } + + /** + * Checks if the given string is a URL + * + * @param string|null $string + * @return bool + */ + public static function isURL(string $string = null): bool + { + return filter_var($string, FILTER_VALIDATE_URL); + } + + /** + * Convert a string to kebab case. + * + * @param string $value + * @return string + */ + public static function kebab(string $value = null): string + { + return static::snake($value, '-'); + } + + /** + * A UTF-8 safe version of strlen() + * + * @param string $string + * @return int + */ + public static function length(string $string = null): int + { + return mb_strlen($string, 'UTF-8'); + } + + /** + * A UTF-8 safe version of strtolower() + * + * @param string $string + * @return string + */ + public static function lower(string $string = null): string + { + return mb_strtolower($string, 'UTF-8'); + } + + /** + * Safe ltrim alternative + * + * @param string $string + * @param string $trim + * @return string + */ + public static function ltrim(string $string, string $trim = ' '): string + { + return preg_replace('!^(' . preg_quote($trim) . ')+!', '', $string); + } + + + /** + * Get a character pool with various possible combinations + * + * @param string|array $type + * @param bool $array + * @return string|array + */ + public static function pool($type, bool $array = true) + { + $pool = []; + + if (is_array($type) === true) { + foreach ($type as $t) { + $pool = array_merge($pool, static::pool($t)); + } + + return $pool; + } else { + switch ($type) { + case 'alphaLower': + $pool = range('a', 'z'); + break; + case 'alphaUpper': + $pool = range('A', 'Z'); + break; + case 'alpha': + $pool = static::pool(['alphaLower', 'alphaUpper']); + break; + case 'num': + $pool = range(0, 9); + break; + case 'alphaNum': + $pool = static::pool(['alpha', 'num']); + break; + } + } + + return $array ? $pool : implode('', $pool); + } + + /** + * Returns the position of a needle in a string + * if it can be found + * + * @param string $string + * @param string $needle + * @param bool $caseInsensitive + * @return int|bool + */ + public static function position(string $string = null, string $needle, bool $caseInsensitive = false) + { + if ($caseInsensitive === true) { + $string = static::lower($string); + $needle = static::lower($needle); + } + + return mb_strpos($string, $needle, 0, 'UTF-8'); + } + + /** + * Runs a string query. + * Check out the Query class for more information. + * + * @param string $query + * @param array $data + * @return string|null + */ + public static function query(string $query, array $data = []) + { + return (new Query($query, $data))->result(); + } + + /** + * Generates a random string that may be used for cryptographic purposes + * + * @param int $length The length of the random string + * @param string $type Pool type (type of allowed characters) + * @return string + */ + public static function random(int $length = null, string $type = 'alphaNum') + { + if ($length === null) { + $length = random_int(5, 10); + } + + $pool = static::pool($type, false); + + // catch invalid pools + if (!$pool) { + return false; + } + + // regex that matches all characters *not* in the pool of allowed characters + $regex = '/[^' . $pool . ']/'; + + // collect characters until we have our required length + $result = ''; + + while (($currentLength = strlen($result)) < $length) { + $missing = $length - $currentLength; + $bytes = random_bytes($missing); + $result .= substr(preg_replace($regex, '', base64_encode($bytes)), 0, $missing); + } + + return $result; + } + + /** + * Replaces all or some occurrences of the search string with the replacement string + * Extension of the str_replace() function in PHP with an additional $limit parameter + * + * @param string|array $string String being replaced on (haystack); + * can be an array of multiple subject strings + * @param string|array $search Value being searched for (needle) + * @param string|array $replace Value to replace matches with + * @param int|array $limit Maximum possible replacements for each search value; + * multiple limits for each search value are supported; + * defaults to no limit + * @return string|array String with replaced values; + * if $string is an array, array of strings + */ + public static function replace($string, $search, $replace, $limit = -1) + { + // convert Kirby collections to arrays + if (is_a($string, 'Kirby\Toolkit\Collection') === true) { + $string = $string->toArray(); + } + + if (is_a($search, 'Kirby\Toolkit\Collection') === true) { + $search = $search->toArray(); + } + + if (is_a($replace, 'Kirby\Toolkit\Collection') === true) { + $replace = $replace->toArray(); + } + + // without a limit we might as well use the built-in function + if ($limit === -1) { + return str_replace($search, $replace, $string); + } + + // if the limit is zero, the result will be no replacements at all + if ($limit === 0) { + return $string; + } + + // multiple subjects are run separately through this method + if (is_array($string) === true) { + $result = []; + foreach ($string as $s) { + $result[] = static::replace($s, $search, $replace, $limit); + } + return $result; + } + + // build an array of replacements + // we don't use an associative array because otherwise you couldn't + // replace the same string with different replacements + $replacements = static::replacements($search, $replace, $limit); + + // run the string and the replacement array through the replacer + return static::replaceReplacements($string, $replacements); + } + + /** + * Generates a replacement array out of dynamic input data + * Used for Str::replace() + * + * @param string|array $search Value being searched for (needle) + * @param string|array $replace Value to replace matches with + * @param int|array $limit Maximum possible replacements for each search value; + * multiple limits for each search value are supported; + * defaults to no limit + * @return array List of replacement arrays, each with a + * 'search', 'replace' and 'limit' attribute + */ + public static function replacements($search, $replace, $limit): array + { + $replacements = []; + + if (is_array($search) === true && is_array($replace) === true) { + foreach ($search as $i => $s) { + // replace with an empty string if no replacement string was defined for this index; + // behavior is identical to the official PHP str_replace() + $r = $replace[$i] ?? ''; + + if (is_array($limit) === true) { + // don't apply a limit if no limit was defined for this index + $l = $limit[$i] ?? -1; + } else { + $l = $limit; + } + + $replacements[] = ['search' => $s, 'replace' => $r, 'limit' => $l]; + } + } elseif (is_array($search) === true && is_string($replace) === true) { + foreach ($search as $i => $s) { + if (is_array($limit) === true) { + // don't apply a limit if no limit was defined for this index + $l = $limit[$i] ?? -1; + } else { + $l = $limit; + } + + $replacements[] = ['search' => $s, 'replace' => $replace, 'limit' => $l]; + } + } elseif (is_string($search) === true && is_string($replace) === true && is_int($limit) === true) { + $replacements[] = compact('search', 'replace', 'limit'); + } else { + throw new Exception('Invalid combination of $search, $replace and $limit params.'); + } + + return $replacements; + } + + /** + * Takes a replacement array and processes the replacements + * Used for Str::replace() + * + * @param string $string String being replaced on (haystack) + * @param array $replacements Replacement array from Str::replacements() + * @return string String with replaced values + */ + public static function replaceReplacements(string $string, array $replacements): string + { + // replace in the order of the replacements + // behavior is identical to the official PHP str_replace() + foreach ($replacements as $replacement) { + if (is_int($replacement['limit']) === false) { + throw new Exception('Invalid limit "' . $replacement['limit'] . '".'); + } elseif ($replacement['limit'] === -1) { + + // no limit, we don't need our special replacement routine + $string = str_replace($replacement['search'], $replacement['replace'], $string); + } elseif ($replacement['limit'] > 0) { + + // limit given, only replace for $replacement['limit'] times per replacement + $position = -1; + + for ($i = 0; $i < $replacement['limit']; $i++) { + $position = strpos($string, $replacement['search'], $position + 1); + + if (is_int($position) === true) { + $string = substr_replace($string, $replacement['replace'], $position, strlen($replacement['search'])); + // adapt $pos to the now changed offset + $position = $position + strlen($replacement['replace']) - strlen($replacement['search']); + } else { + // no more match in the string + break; + } + } + } + } + + return $string; + } + + /** + * Safe rtrim alternative + * + * @param string $string + * @param string $trim + * @return string + */ + public static function rtrim(string $string, string $trim = ' '): string + { + return preg_replace('!(' . preg_quote($trim) . ')+$!', '', $string); + } + + /** + * Shortens a string and adds an ellipsis if the string is too long + * + * + * + * echo Str::short('This is a very, very, very long string', 10); + * // output: This is a… + * + * echo Str::short('This is a very, very, very long string', 10, '####'); + * // output: This i#### + * + * + * + * @param string $string The string to be shortened + * @param int $length The final number of characters the + * string should have + * @param string $appendix The element, which should be added if the + * string is too long. Ellipsis is the default. + * @return string The shortened string + */ + public static function short(string $string = null, int $length = 0, string $appendix = '…'): ?string + { + if ($length === 0) { + return $string; + } + + if (static::length($string) <= $length) { + return $string; + } + + return static::substr($string, 0, $length) . $appendix; + } + + /** + * Convert a string to a safe version to be used in a URL + * + * @param string $string The unsafe string + * @param string $separator To be used instead of space and + * other non-word characters. + * @param string $allowed List of all allowed characters (regex) + * @param int $maxlength The maximum length of the slug + * @return string The safe string + */ + public static function slug(string $string = null, string $separator = null, string $allowed = null, int $maxlength = 128): string + { + $separator = $separator ?? static::$defaults['slug']['separator']; + $allowed = $allowed ?? static::$defaults['slug']['allowed']; + + $string = trim($string); + $string = static::lower($string); + $string = static::ascii($string); + + // replace spaces with simple dashes + $string = preg_replace('![^' . $allowed . ']!i', $separator, $string); + + if (strlen($separator) > 0) { + // remove double separators + $string = preg_replace('![' . preg_quote($separator) . ']{2,}!', $separator, $string); + } + + // replace slashes with dashes + $string = str_replace('/', $separator, $string); + + // trim leading and trailing non-word-chars + $string = preg_replace('!^[^a-z0-9]+!', '', $string); + $string = preg_replace('![^a-z0-9]+$!', '', $string); + + // cut the string after the given maxlength + return static::short($string, $maxlength, false); + } + + /** + * Convert a string to snake case. + * + * @param string $value + * @param string $delimiter + * @return string + */ + public static function snake(string $value = null, string $delimiter = '_'): string + { + if (!ctype_lower($value)) { + $value = preg_replace('/\s+/u', '', ucwords($value)); + $value = static::lower(preg_replace('/(.)(?=[A-Z])/u', '$1' . $delimiter, $value)); + } + return $value; + } + + /** + * Better alternative for explode() + * It takes care of removing empty values + * and it has a built-in way to skip values + * which are too short. + * + * @param string $string The string to split + * @param string $separator The string to split by + * @param int $length The min length of values. + * @return array An array of found values + */ + public static function split($string, string $separator = ',', int $length = 1): array + { + if (is_array($string) === true) { + return $string; + } + + $parts = explode($separator, $string); + $out = []; + + foreach ($parts as $p) { + $p = trim($p); + if (static::length($p) > 0 && static::length($p) >= $length) { + $out[] = $p; + } + } + + return $out; + } + + /** + * Checks if a string starts with the passed needle + * + * @param string $string + * @param string $needle + * @param bool $caseInsensitive + * @return bool + */ + public static function startsWith(string $string = null, string $needle, bool $caseInsensitive = false): bool + { + if ($needle === '') { + return true; + } + + return static::position($string, $needle, $caseInsensitive) === 0; + } + + /** + * A UTF-8 safe version of substr() + * + * @param string $string + * @param int $start + * @param int $length + * @return string + */ + public static function substr(string $string = null, int $start = 0, int $length = null): string + { + return mb_substr($string, $start, $length, 'UTF-8'); + } + + /** + * Replaces placeholders in string with value from array + * + * + * + * echo Str::template('From {{ b }} to {{ a }}', ['a' => 'there', 'b' => 'here']); + * // output: From here to there + * + * + * + * @param string $string The string with placeholders + * @param array $data Associative array with placeholders as + * keys and replacements as values + * @param string $fallback A fallback if a token does not have any matches + * @param string $start Placeholder start characters + * @param string $end Placeholder end characters + * @return string The filled-in string + */ + public static function template(string $string = null, array $data = [], string $fallback = null, string $start = '{{', string $end = '}}'): string + { + return preg_replace_callback('!' . $start . '(.*?)' . $end . '!', function ($match) use ($data, $fallback) { + $query = trim($match[1]); + if (strpos($query, '.') !== false) { + return (new Query($match[1], $data))->result() ?? $fallback; + } + return $data[$query] ?? $fallback; + }, $string); + } + + /** + * Converts a filesize string with shortcuts + * like M, G or K to an integer value + * + * @param mixed $size + * @return int + */ + public static function toBytes($size): int + { + $size = trim($size); + $last = strtolower($size[strlen($size)-1] ?? null); + $size = (int)$size; + + switch ($last) { + case 'g': + $size *= 1024; + // no break + case 'm': + $size *= 1024; + // no break + case 'k': + $size *= 1024; + } + + return $size; + } + + /** + * Convert the string to the given type + * + * @param string $string + * @param mixed $type + * @return mixed + */ + public static function toType($string = null, $type) + { + if (is_string($type) === false) { + $type = gettype($type); + } + + switch ($type) { + case 'array': + return (array)$string; + case 'bool': + case 'boolean': + return filter_var($string, FILTER_VALIDATE_BOOLEAN); + case 'double': + case 'float': + return (float)$string; + case 'int': + case 'integer': + return (int)$string; + } + + return (string)$string; + } + + /** + * Safe trim alternative + * + * @param string $string + * @param string $trim + * @return string + */ + public static function trim(string $string, string $trim = ' '): string + { + return static::rtrim(static::ltrim($string, $trim), $trim); + } + + /** + * A UTF-8 safe version of ucfirst() + * + * @param string $string + * @return string + */ + public static function ucfirst(string $string = null): string + { + return static::upper(static::substr($string, 0, 1)) . static::lower(static::substr($string, 1)); + } + + /** + * A UTF-8 safe version of ucwords() + * + * @param string $string + * @return string + */ + public static function ucwords(string $string = null): string + { + return mb_convert_case($string, MB_CASE_TITLE, 'UTF-8'); + } + + /** + * Removes all html tags and encoded chars from a string + * + * + * + * echo str::unhtml('some crazy stuff'); + * // output: some uber crazy stuff + * + * + * + * @param string $string + * @return string The html string + */ + public static function unhtml(string $string = null): string + { + return Html::decode($string); + } + + /** + * Returns the beginning of a string until the given character + * + * @param string $string + * @param string $needle + * @param bool $caseInsensitive + * @return string + */ + public static function until(string $string, string $needle, bool $caseInsensitive = false): string + { + $position = static::position($string, $needle, $caseInsensitive); + + if ($position === false) { + return false; + } else { + return static::substr($string, 0, $position + static::length($needle)); + } + } + + /** + * A UTF-8 safe version of strotoupper() + * + * @param string $string + * @return string + */ + public static function upper(string $string = null): string + { + return mb_strtoupper($string, 'UTF-8'); + } + + /** + * 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 $string + * @return string + */ + public static function widont(string $string = null): string + { + return preg_replace_callback('|([^\s])\s+([^\s]+)\s*$|u', function ($matches) { + if (static::contains($matches[2], '-')) { + return $matches[1] . ' ' . str_replace('-', '‑', $matches[2]); + } else { + return $matches[1] . ' ' . $matches[2]; + } + }, $string); + } +} diff --git a/kirby/src/Toolkit/Tpl.php b/kirby/src/Toolkit/Tpl.php new file mode 100755 index 0000000..41816f3 --- /dev/null +++ b/kirby/src/Toolkit/Tpl.php @@ -0,0 +1,51 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Tpl +{ + /** + * Renders the template + * + * @param string $__file + * @param array $__data + * @return string + */ + public static function load(string $__file = null, array $__data = []): string + { + if (file_exists($__file) === false) { + return ''; + } + + $exception = null; + + ob_start(); + extract($__data); + + try { + require $__file; + } catch (Throwable $e) { + $exception = $e; + } + + $content = ob_get_contents(); + ob_end_clean(); + + if ($exception === null) { + return $content; + } + + throw $exception; + } +} diff --git a/kirby/src/Toolkit/V.php b/kirby/src/Toolkit/V.php new file mode 100755 index 0000000..37ba853 --- /dev/null +++ b/kirby/src/Toolkit/V.php @@ -0,0 +1,488 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class V +{ + /** + * An array with all installed validators + * + * @var array + */ + public static $validators = []; + + /** + * Validates the given input with all passed rules + * and returns an array with all error messages. + * The array will be empty if the input is valid + * + * @param mixed $input + * @param array $rules + * @param array $messages + * @return array + */ + public static function errors($input, array $rules, $messages = []): array + { + $errors = static::value($input, $rules, $messages, false); + + return $errors === true ? [] : $errors; + } + + /** + * Creates a useful error message for the given validator + * and the arguments. This is used mainly internally + * to create error messages + * + * @param string $validatorName + * @param mixed ...$params + * @return string|null + */ + public static function message(string $validatorName, ...$params): ?string + { + $validatorName = strtolower($validatorName); + $translationKey = 'error.validation.' . $validatorName; + $validators = array_change_key_case(static::$validators); + $validator = $validators[$validatorName] ?? null; + + if ($validator === null) { + return null; + } + + $reflection = new ReflectionFunction($validator); + $arguments = []; + + foreach ($reflection->getParameters() as $index => $parameter) { + $value = $params[$index] ?? null; + + if (is_array($value) === true) { + try { + foreach ($value as $index => $item) { + if (is_array($item) === true) { + $value[$index] = implode('|', $item); + } + } + $value = implode(', ', $value); + } catch (Throwable $e) { + $value = '-'; + } + } + + $arguments[$parameter->getName()] = $value; + } + + return I18n::template($translationKey, 'The "' . $validatorName . '" validation failed', $arguments); + } + + /** + * Return the list of all validators + * + * @return array + */ + public static function validators(): array + { + return static::$validators; + } + + /** + * Validate a single value against + * a set of rules, using all registered + * validators + * + * @param mixed $value + * @param array $rules + * @param array $messages + * @param bool $fail + * @return bool|array + */ + public static function value($value, array $rules, array $messages = [], bool $fail = true) + { + $errors = []; + + foreach ($rules as $validatorName => $validatorOptions) { + if (is_int($validatorName)) { + $validatorName = $validatorOptions; + $validatorOptions = []; + } + + if (is_array($validatorOptions) === false) { + $validatorOptions = [$validatorOptions]; + } + + $validatorName = strtolower($validatorName); + + if (static::$validatorName($value, ...$validatorOptions) === false) { + $message = $messages[$validatorName] ?? static::message($validatorName, $value, ...$validatorOptions); + $errors[$validatorName] = $message; + + if ($fail === true) { + throw new Exception($message); + } + } + } + + return empty($errors) === true ? true : $errors; + } + + /** + * Validate an input array against + * a set of rules, using all registered + * validators + * + * @param array $input + * @param array $rules + * @return bool + */ + public static function input(array $input, array $rules): bool + { + foreach ($rules as $fieldName => $fieldRules) { + $fieldValue = $input[$fieldName] ?? null; + + // first check for required fields + if (($fieldRules['required'] ?? false) === true && $fieldValue === null) { + throw new Exception(sprintf('The "%s" field is missing', $fieldName)); + } + + // remove the required rule + unset($fieldRules['required']); + + // skip validation for empty fields + if ($fieldValue === null) { + continue; + } + + try { + V::value($fieldValue, $fieldRules); + } catch (Exception $e) { + throw new Exception(sprintf($e->getMessage() . ' for field "%s"', $fieldName)); + } + + static::value($fieldValue, $fieldRules); + } + + return true; + } + + /** + * Calls an installed validator and passes all arguments + * + * @param string $method + * @param array $arguments + * @return bool + */ + public static function __callStatic(string $method, array $arguments): bool + { + $method = strtolower($method); + $validators = array_change_key_case(static::$validators); + + // check for missing validators + if (isset($validators[$method]) === false) { + throw new Exception('The validator does not exist: ' . $method); + } + + return call_user_func_array($validators[$method], $arguments); + } +} + + +/** + * Default set of validators + */ +V::$validators = [ + /** + * Valid: `'yes' | true | 1 | 'on'` + */ + 'accepted' => function ($value): bool { + return V::in($value, [1, true, 'yes', 'true', '1', 'on'], true) === true; + }, + + /** + * Valid: `a-z | A-Z` + */ + 'alpha' => function ($value): bool { + return V::match($value, '/^([a-z])+$/i') === true; + }, + + /** + * Valid: `a-z | A-Z | 0-9` + */ + 'alphanum' => function ($value): bool { + return V::match($value, '/^[a-z0-9]+$/i') === true; + }, + + /** + * Checks for numbers within the given range + */ + 'between' => function ($value, $min, $max): bool { + return V::min($value, $min) === true && + V::max($value, $max) === true; + }, + + /** + * Checks if the given string contains the given value + */ + 'contains' => function ($value, $needle): bool { + return Str::contains($value, $needle); + }, + + /** + * Checks for a valid date + */ + 'date' => function ($value): bool { + $date = date_parse($value); + return $date !== false && + $date['error_count'] === 0 && + $date['warning_count'] === 0; + }, + + /** + * Valid: `'no' | false | 0 | 'off'` + */ + 'denied' => function ($value): bool { + return V::in($value, [0, false, 'no', 'false', '0', 'off'], true) === true; + }, + + /** + * Checks for a value, which does not equal the given value + */ + 'different' => function ($value, $other, $strict = false): bool { + if ($strict === true) { + return $value !== $other; + } + return $value != $other; + }, + + /** + * Checks for valid email addresses + */ + 'email' => function ($value): bool { + if (filter_var($value, FILTER_VALIDATE_EMAIL) === false) { + try { + $parts = Str::split($value, '@'); + $address = $parts[0] ?? null; + $domain = Idn::encode($parts[1] ?? ''); + $email = $address . '@' . $domain; + } catch (Throwable $e) { + return false; + } + + return filter_var($email, FILTER_VALIDATE_EMAIL) !== false; + } + + return true; + }, + + /** + * Checks if the given string ends with the given value + */ + 'endsWith' => function (string $value, string $end): bool { + return Str::endsWith($value, $end); + }, + + /** + * Checks for a valid filename + */ + 'filename' => function ($value): bool { + return V::match($value, '/^[a-z0-9@._-]+$/i') === true && + V::min($value, 2) === true; + }, + + /** + * Checks if the value exists in a list of given values + */ + 'in' => function ($value, array $in, bool $strict = false): bool { + return in_array($value, $in, $strict) === true; + }, + + /** + * Checks for a valid integer + */ + 'integer' => function ($value, bool $strict = false): bool { + if ($strict === true) { + return is_int($value) === true; + } + return filter_var($value, FILTER_VALIDATE_INT) !== false; + }, + + /** + * Checks for a valid IP address + */ + 'ip' => function ($value): bool { + return filter_var($value, FILTER_VALIDATE_IP) !== false; + }, + + /** + * Checks if the value is lower than the second value + */ + 'less' => function ($value, float $max): bool { + return V::size($value, $max, '<') === true; + }, + + /** + * Checks if the value matches the given regular expression + */ + 'match' => function ($value, string $pattern): bool { + return preg_match($pattern, $value) !== 0; + }, + + /** + * Checks if the value does not exceed the maximum value + */ + 'max' => function ($value, float $max): bool { + return V::size($value, $max, '<=') === true; + }, + + /** + * Checks if the value is higher than the minimum value + */ + 'min' => function ($value, float $min): bool { + return V::size($value, $min, '>=') === true; + }, + + /** + * Checks if the number of characters in the value equals or is below the given maximum + */ + 'maxLength' => function (string $value = null, $max): bool { + return Str::length(trim($value)) <= $max; + }, + + /** + * Checks if the number of characters in the value equals or is greater than the given minimum + */ + 'minLength' => function (string $value = null, $min): bool { + return Str::length(trim($value)) >= $min; + }, + + /** + * Checks if the number of words in the value equals or is below the given maximum + */ + 'maxWords' => function (string $value = null, $max): bool { + return V::max(explode(' ', trim($value)), $max) === true; + }, + + /** + * Checks if the number of words in the value equals or is below the given maximum + */ + 'minWords' => function (string $value = null, $min): bool { + return V::min(explode(' ', trim($value)), $min) === true; + }, + + /** + * Checks if the first value is higher than the second value + */ + 'more' => function ($value, float $min): bool { + return V::size($value, $min, '>') === true; + }, + + /** + * Checks that the given string does not contain the second value + */ + 'notContains' => function ($value, $needle): bool { + return V::contains($value, $needle) === false; + }, + + /** + * Checks that the given value is not in the given list of values + */ + 'notIn' => function ($value, $notIn): bool { + return V::in($value, $notIn) === false; + }, + + /** + * Checks for a valid number / numeric value (float, int, double) + */ + 'num' => function ($value): bool { + return is_numeric($value) === true; + }, + + /** + * Checks if the value is present in the given array + */ + 'required' => function ($key, array $array): bool { + return isset($array[$key]) === true && + V::notIn($array[$key], [null, '', []]) === true; + }, + + /** + * Checks that the first value equals the second value + */ + 'same' => function ($value, $other, bool $strict = false): bool { + if ($strict === true) { + return $value === $other; + } + return $value == $other; + }, + + /** + * Checks that the value has the given size + */ + 'size' => function ($value, $size, $operator = '=='): bool { + if (is_numeric($value) === true) { + $count = $value; + } elseif (is_string($value) === true) { + $count = Str::length(trim($value)); + } elseif (is_array($value) === true) { + $count = count($value); + } elseif (is_object($value) === true) { + if ($value instanceof \Countable) { + $count = count($value); + } elseif (method_exists($value, 'count') === true) { + $count = $value->count(); + } else { + throw new Exception('$value is an uncountable object'); + } + } else { + throw new Exception('$value is of type without size'); + } + + switch ($operator) { + case '<': + return $count < $size; + case '>': + return $count > $size; + case '<=': + return $count <= $size; + case '>=': + return $count >= $size; + default: + return $count == $size; + } + }, + + /** + * Checks that the string starts with the given start value + */ + 'startsWith' => function (string $value, string $start): bool { + return Str::startsWith($value, $start); + }, + + /** + * Checks for valid time + */ + 'time' => function ($value): bool { + return V::date($value); + }, + + /** + * Checks for a valid Url + */ + 'url' => function ($value): bool { + // In search for the perfect regular expression: https://mathiasbynens.be/demo/url-regex + // Added localhost support and removed 127.*.*.* ip restriction + $regex = '_^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!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]))|(?:localhost)|(?:(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)*(?:\.(?:[a-z\x{00a1}-\x{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$_iu'; + return preg_match($regex, $value) !== 0; + } +]; diff --git a/kirby/src/Toolkit/View.php b/kirby/src/Toolkit/View.php new file mode 100755 index 0000000..db54591 --- /dev/null +++ b/kirby/src/Toolkit/View.php @@ -0,0 +1,138 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class View +{ + /** + * The absolute path to the view file + * + * @var string + */ + protected $file; + + /** + * The view data + * + * @var array + */ + protected $data = []; + + /** + * Creates a new view object + * + * @param string $file + * @param array $data + */ + public function __construct(string $file, array $data = []) + { + $this->file = $file; + $this->data = $data; + } + + /** + * Returns the view's data array + * without globals. + * + * @return array + */ + public function data(): array + { + return $this->data; + } + + /** + * Checks if the template file exists + * + * @return bool + */ + public function exists(): bool + { + return file_exists($this->file()) === true; + } + + /** + * Returns the view file + * + * @return string|false + */ + public function file() + { + return $this->file; + } + + /** + * Creates an error message for the missing view exception + * + * @return string + */ + protected function missingViewMessage(): string + { + return 'The view does not exist: ' . $this->file(); + } + + /** + * Renders the view + * + * @return string + */ + public function render(): string + { + if ($this->exists() === false) { + throw new Exception($this->missingViewMessage()); + } + + $exception = null; + + ob_start(); + extract($this->data()); + + try { + require $this->file(); + } catch (Throwable $e) { + $exception = $e; + } + + $content = ob_get_contents(); + ob_end_clean(); + + if ($exception === null) { + return $content; + } + + throw $exception; + } + + /** + * Alias for View::render() + * + * @return string + */ + public function toString(): string + { + return $this->render(); + } + + /** + * Magic string converter to enable + * converting view objects to string + * + * @return string + */ + public function __toString(): string + { + return $this->render(); + } +} diff --git a/kirby/src/Toolkit/Xml.php b/kirby/src/Toolkit/Xml.php new file mode 100755 index 0000000..563f57e --- /dev/null +++ b/kirby/src/Toolkit/Xml.php @@ -0,0 +1,246 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Xml +{ + /** + * Conversion table for html entities + * + * @var array + */ + public static $entities = [ + ' ' => ' ', '¡' => '¡', '¢' => '¢', '£' => '£', '¤' => '¤', '¥' => '¥', '¦' => '¦', '§' => '§', + '¨' => '¨', '©' => '©', 'ª' => 'ª', '«' => '«', '¬' => '¬', '­' => '­', '®' => '®', '¯' => '¯', + '°' => '°', '±' => '±', '²' => '²', '³' => '³', '´' => '´', 'µ' => 'µ', '¶' => '¶', '·' => '·', + '¸' => '¸', '¹' => '¹', 'º' => 'º', '»' => '»', '¼' => '¼', '½' => '½', '¾' => '¾', '¿' => '¿', + 'À' => 'À', 'Á' => 'Á', 'Â' => 'Â', 'Ã' => 'Ã', 'Ä' => 'Ä', 'Å' => 'Å', 'Æ' => 'Æ', 'Ç' => 'Ç', + 'È' => 'È', 'É' => 'É', 'Ê' => 'Ê', 'Ë' => 'Ë', 'Ì' => 'Ì', 'Í' => 'Í', 'Î' => 'Î', 'Ï' => 'Ï', + 'Ð' => 'Ð', 'Ñ' => 'Ñ', 'Ò' => 'Ò', 'Ó' => 'Ó', 'Ô' => 'Ô', 'Õ' => 'Õ', 'Ö' => 'Ö', '×' => '×', + 'Ø' => 'Ø', 'Ù' => 'Ù', 'Ú' => 'Ú', 'Û' => 'Û', 'Ü' => 'Ü', 'Ý' => 'Ý', 'Þ' => 'Þ', 'ß' => 'ß', + 'à' => 'à', 'á' => 'á', 'â' => 'â', 'ã' => 'ã', 'ä' => 'ä', 'å' => 'å', 'æ' => 'æ', 'ç' => 'ç', + 'è' => 'è', 'é' => 'é', 'ê' => 'ê', 'ë' => 'ë', 'ì' => 'ì', 'í' => 'í', 'î' => 'î', 'ï' => 'ï', + 'ð' => 'ð', 'ñ' => 'ñ', 'ò' => 'ò', 'ó' => 'ó', 'ô' => 'ô', 'õ' => 'õ', 'ö' => 'ö', '÷' => '÷', + 'ø' => 'ø', 'ù' => 'ù', 'ú' => 'ú', 'û' => 'û', 'ü' => 'ü', 'ý' => 'ý', 'þ' => 'þ', 'ÿ' => 'ÿ', + 'ƒ' => 'ƒ', 'Α' => 'Α', 'Β' => 'Β', 'Γ' => 'Γ', 'Δ' => 'Δ', 'Ε' => 'Ε', 'Ζ' => 'Ζ', 'Η' => 'Η', + 'Θ' => 'Θ', 'Ι' => 'Ι', 'Κ' => 'Κ', 'Λ' => 'Λ', 'Μ' => 'Μ', 'Ν' => 'Ν', 'Ξ' => 'Ξ', 'Ο' => 'Ο', + 'Π' => 'Π', 'Ρ' => 'Ρ', 'Σ' => 'Σ', 'Τ' => 'Τ', 'Υ' => 'Υ', 'Φ' => 'Φ', 'Χ' => 'Χ', 'Ψ' => 'Ψ', + 'Ω' => 'Ω', 'α' => 'α', 'β' => 'β', 'γ' => 'γ', 'δ' => 'δ', 'ε' => 'ε', 'ζ' => 'ζ', 'η' => 'η', + 'θ' => 'θ', 'ι' => 'ι', 'κ' => 'κ', 'λ' => 'λ', 'μ' => 'μ', 'ν' => 'ν', 'ξ' => 'ξ', 'ο' => 'ο', + 'π' => 'π', 'ρ' => 'ρ', 'ς' => 'ς', 'σ' => 'σ', 'τ' => 'τ', 'υ' => 'υ', 'φ' => 'φ', 'χ' => 'χ', + 'ψ' => 'ψ', 'ω' => 'ω', 'ϑ' => 'ϑ', 'ϒ' => 'ϒ', 'ϖ' => 'ϖ', '•' => '•', '…' => '…', '′' => '′', + '″' => '″', '‾' => '‾', '⁄' => '⁄', '℘' => '℘', 'ℑ' => 'ℑ', 'ℜ' => 'ℜ', '™' => '™', 'ℵ' => 'ℵ', + '←' => '←', '↑' => '↑', '→' => '→', '↓' => '↓', '↔' => '↔', '↵' => '↵', '⇐' => '⇐', '⇑' => '⇑', + '⇒' => '⇒', '⇓' => '⇓', '⇔' => '⇔', '∀' => '∀', '∂' => '∂', '∃' => '∃', '∅' => '∅', '∇' => '∇', + '∈' => '∈', '∉' => '∉', '∋' => '∋', '∏' => '∏', '∑' => '∑', '−' => '−', '∗' => '∗', '√' => '√', + '∝' => '∝', '∞' => '∞', '∠' => '∠', '∧' => '∧', '∨' => '∨', '∩' => '∩', '∪' => '∪', '∫' => '∫', + '∴' => '∴', '∼' => '∼', '≅' => '≅', '≈' => '≈', '≠' => '≠', '≡' => '≡', '≤' => '≤', '≥' => '≥', + '⊂' => '⊂', '⊃' => '⊃', '⊄' => '⊄', '⊆' => '⊆', '⊇' => '⊇', '⊕' => '⊕', '⊗' => '⊗', '⊥' => '⊥', + '⋅' => '⋅', '⌈' => '⌈', '⌉' => '⌉', '⌊' => '⌊', '⌋' => '⌋', '⟨' => '〈', '⟩' => '〉', '◊' => '◊', + '♠' => '♠', '♣' => '♣', '♥' => '♥', '♦' => '♦', '"' => '"', '&' => '&', '<' => '<', '>' => '>', 'Œ' => 'Œ', + 'œ' => 'œ', 'Š' => 'Š', 'š' => 'š', 'Ÿ' => 'Ÿ', 'ˆ' => 'ˆ', '˜' => '˜', ' ' => ' ', ' ' => ' ', + ' ' => ' ', '‌' => '‌', '‍' => '‍', '‎' => '‎', '‏' => '‏', '–' => '–', '—' => '—', '‘' => '‘', + '’' => '’', '‚' => '‚', '“' => '“', '”' => '”', '„' => '„', '†' => '†', '‡' => '‡', '‰' => '‰', + '‹' => '‹', '›' => '›', '€' => '€' + ]; + + /** + * Creates an XML string from an array + * + * @param string $props The source array + * @param string $name The name of the root element + * @param bool $head Include the xml declaration head or not + * @param int $level The indendation level + * @return string The XML string + */ + public static function create($props, string $name = 'root', bool $head = true, $level = 0): string + { + $attributes = $props['@attributes'] ?? null; + $value = $props['@value'] ?? null; + $children = $props; + $indent = str_repeat(' ', $level); + $nextLevel = $level + 1; + + if (is_array($children) === true) { + unset($children['@attributes'], $children['@value']); + + $childTags = []; + + foreach ($children as $childName => $childItems) { + if (is_array($childItems) === true) { + + // another tag with attributes + if (A::isAssociative($childItems) === true) { + $childTags[] = static::create($childItems, $childName, false, $level); + + // just children + } else { + foreach ($childItems as $childItem) { + $childTags[] = static::create($childItem, $childName, false, $nextLevel); + } + } + } else { + $childTags[] = static::tag($childName, $childItems, null, $indent); + } + } + + if (empty($childTags) === false) { + $value = $childTags; + } + } + + $result = $head === true ? '' . PHP_EOL : null; + $result .= static::tag($name, $value, $attributes, $indent); + + return $result; + } + + /** + * Removes all xml entities from a string + * and convert them to html entities first + * and remove all html entities afterwards. + * + * + * + * echo xml::decode('some über crazy stuff'); + * // output: some über crazy stuff + * + * + * + * @param string $string + * @return string + */ + public static function decode(string $string = null): string + { + return Html::decode($string); + } + + /** + * Converts a string to a xml-safe string + * Converts it to html-safe first and then it + * will replace html entities to xml entities + * + * + * + * echo xml::encode('some über crazy stuff'); + * // output: some über crazy stuff + * + * + * + * @param string $string + * @param bool $html True: convert to html first + * @return string + */ + public static function encode(string $string = null, bool $html = true): string + { + if ($html === true) { + $string = Html::encode($string, false); + } + + $entities = static::entities(); + $searches = array_keys($entities); + $values = array_values($entities); + + return str_replace($searches, $values, $string); + } + + /** + * Returns the html to xml entities translation table + * + * @return array + */ + public static function entities(): array + { + return static::$entities; + } + + /** + * Parses a XML string and returns an array + * + * @param string $xml + * @return array|false + */ + public static function parse(string $xml = null) + { + $xml = preg_replace('/(<\/?)(\w+):([^>]*>)/', '$1$2$3', $xml); + $xml = @simplexml_load_string($xml, null, LIBXML_NOENT | LIBXML_NOCDATA); + + $xml = @json_encode($xml); + $xml = @json_decode($xml, true); + return is_array($xml) === true ? $xml : false; + } + + /** + * Builds an XML tag + * + * @param string $name + * @param mixed $content + * @param array $attr + * @param mixed $indent + * @return string + */ + public static function tag(string $name, $content = null, array $attr = null, $indent = null): string + { + $attr = Html::attr($attr); + $start = '<' . $name . ($attr ? ' ' . $attr : null) . '>'; + $end = ''; + + if (is_array($content) === true) { + $xml = $indent . $start . PHP_EOL; + foreach ($content as $line) { + $xml .= $indent . $indent . $line . PHP_EOL; + } + $xml .= $indent . $end; + } else { + $xml = $indent . $start . static::value($content) . $end; + } + + return $xml; + } + + /** + * Encodes the value as cdata if necessary + * + * @param mixed $value + * @return mixed + */ + public static function value($value) + { + if ($value === true) { + return 'true'; + } + + if ($value === false) { + return 'false'; + } + + if (is_numeric($value) === true) { + return $value; + } + + if ($value === null || $value === '') { + return null; + } + + if (Str::contains($value, ''; + } +} diff --git a/kirby/vendor/autoload.php b/kirby/vendor/autoload.php new file mode 100755 index 0000000..e15730a --- /dev/null +++ b/kirby/vendor/autoload.php @@ -0,0 +1,7 @@ +. +// +// Copyright A Beautiful Site, LLC. +// +// Source: https://github.com/claviska/SimpleImage +// +// Licensed under the MIT license +// + +namespace claviska; + +class SimpleImage { + + const + ERR_FILE_NOT_FOUND = 1, + ERR_FONT_FILE = 2, + ERR_FREETYPE_NOT_ENABLED = 3, + ERR_GD_NOT_ENABLED = 4, + ERR_INVALID_COLOR = 5, + ERR_INVALID_DATA_URI = 6, + ERR_INVALID_IMAGE = 7, + ERR_LIB_NOT_LOADED = 8, + ERR_UNSUPPORTED_FORMAT = 9, + ERR_WEBP_NOT_ENABLED = 10, + ERR_WRITE = 11; + + protected $image, $mimeType, $exif; + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Magic methods + ////////////////////////////////////////////////////////////////////////////////////////////////// + + // + // Creates a new SimpleImage object. + // + // $image (string) - An image file or a data URI to load. + // + public function __construct($image = null) { + // Check for the required GD extension + if(extension_loaded('gd')) { + // Ignore JPEG warnings that cause imagecreatefromjpeg() to fail + ini_set('gd.jpeg_ignore_warning', 1); + } else { + throw new \Exception('Required extension GD is not loaded.', self::ERR_GD_NOT_ENABLED); + } + + // Load an image through the constructor + if(preg_match('/^data:(.*?);/', $image)) { + $this->fromDataUri($image); + } elseif($image) { + $this->fromFile($image); + } + } + + // + // Destroys the image resource + // + public function __destruct() { + if($this->image !== null && get_resource_type($this->image) === 'gd') { + imagedestroy($this->image); + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Loaders + ////////////////////////////////////////////////////////////////////////////////////////////////// + + // + // Loads an image from a data URI. + // + // $uri* (string) - A data URI. + // + // Returns a SimpleImage object. + // + public function fromDataUri($uri) { + // Basic formatting check + preg_match('/^data:(.*?);/', $uri, $matches); + if(!count($matches)) { + throw new \Exception('Invalid data URI.', self::ERR_INVALID_DATA_URI); + } + + // Determine mime type + $this->mimeType = $matches[1]; + if(!preg_match('/^image\/(gif|jpeg|png)$/', $this->mimeType)) { + throw new \Exception( + 'Unsupported format: ' . $this->mimeType, + self::ERR_UNSUPPORTED_FORMAT + ); + } + + // Get image data + $uri = base64_decode(preg_replace('/^data:(.*?);base64,/', '', $uri)); + $this->image = imagecreatefromstring($uri); + if(!$this->image) { + throw new \Exception("Invalid image data.", self::ERR_INVALID_IMAGE); + } + + return $this; + } + + // + // Loads an image from a file. + // + // $file* (string) - The image file to load. + // + // Returns a SimpleImage object. + // + public function fromFile($file) { + // Check if the file exists and is readable. We're using fopen() instead of file_exists() + // because not all URL wrappers support the latter. + $handle = @fopen($file, 'r'); + if($handle === false) { + throw new \Exception("File not found: $file", self::ERR_FILE_NOT_FOUND); + } + fclose($handle); + + // Get image info + $info = getimagesize($file); + if($info === false) { + throw new \Exception("Invalid image file: $file", self::ERR_INVALID_IMAGE); + } + $this->mimeType = $info['mime']; + + // Create image object from file + switch($this->mimeType) { + case 'image/gif': + // Load the gif + $gif = imagecreatefromgif($file); + if($gif) { + // Copy the gif over to a true color image to preserve its transparency. This is a + // workaround to prevent imagepalettetruecolor() from borking transparency. + $width = imagesx($gif); + $height = imagesy($gif); + $this->image = imagecreatetruecolor($width, $height); + $transparentColor = imagecolorallocatealpha($this->image, 0, 0, 0, 127); + imagecolortransparent($this->image, $transparentColor); + imagefill($this->image, 0, 0, $transparentColor); + imagecopy($this->image, $gif, 0, 0, 0, 0, $width, $height); + imagedestroy($gif); + } + break; + case 'image/jpeg': + $this->image = imagecreatefromjpeg($file); + break; + case 'image/png': + $this->image = imagecreatefrompng($file); + break; + case 'image/webp': + $this->image = imagecreatefromwebp($file); + break; + } + if(!$this->image) { + throw new \Exception("Unsupported image: $file", self::ERR_UNSUPPORTED_FORMAT); + } + + // Convert pallete images to true color images + imagepalettetotruecolor($this->image); + + // Load exif data from JPEG images + if($this->mimeType === 'image/jpeg' && function_exists('exif_read_data')) { + $this->exif = @exif_read_data($file); + } + + return $this; + } + + // + // Creates a new image. + // + // $width* (int) - The width of the image. + // $height* (int) - The height of the image. + // $color (string|array) - Optional fill color for the new image (default 'transparent'). + // + // Returns a SimpleImage object. + // + public function fromNew($width, $height, $color = 'transparent') { + $this->image = imagecreatetruecolor($width, $height); + + // Use PNG for dynamically created images because it's lossless and supports transparency + $this->mimeType = 'image/png'; + + // Fill the image with color + $this->fill($color); + + return $this; + } + + // + // Creates a new image from a string. + // + // $string* (string) - The raw image data as a string. Example: + // + // $string = file_get_contents('image.jpg'); + // + // Returns a SimpleImage object. + // + public function fromString($string) { + return $this->fromFile('data://;base64,' . base64_encode($string)); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Savers + ////////////////////////////////////////////////////////////////////////////////////////////////// + + // + // Generates an image. + // + // $mimeType (string) - The image format to output as a mime type (defaults to the original mime + // type). + // $quality (int) - Image quality as a percentage (default 100). + // + // Returns an array containing the image data and mime type. + // + private function generate($mimeType = null, $quality = 100) { + // Format defaults to the original mime type + $mimeType = $mimeType ?: $this->mimeType; + + // Ensure quality is a valid integer + if($quality === null) $quality = 100; + $quality = self::keepWithin((int) $quality, 0, 100); + + // Capture output + ob_start(); + + // Generate the image + switch($mimeType) { + case 'image/gif': + imagesavealpha($this->image, true); + imagegif($this->image, null); + break; + case 'image/jpeg': + imageinterlace($this->image, true); + imagejpeg($this->image, null, $quality); + break; + case 'image/png': + imagesavealpha($this->image, true); + imagepng($this->image, null, round(9 * $quality / 100)); + break; + case 'image/webp': + // Not all versions of PHP will have webp support enabled + if(!function_exists('imagewebp')) { + throw new \Exception( + 'WEBP support is not enabled in your version of PHP.', + self::ERR_WEBP_NOT_ENABLED + ); + } + imagesavealpha($this->image, true); + imagewebp($this->image, null, $quality); + break; + default: + throw new \Exception('Unsupported format: ' . $mimeType, self::ERR_UNSUPPORTED_FORMAT); + } + + // Stop capturing + $data = ob_get_contents(); + ob_end_clean(); + + return [ + 'data' => $data, + 'mimeType' => $mimeType + ]; + } + + // + // Generates a data URI. + // + // $mimeType (string) - The image format to output as a mime type (defaults to the original mime + // type). + // $quality (int) - Image quality as a percentage (default 100). + // + // Returns a string containing a data URI. + // + public function toDataUri($mimeType = null, $quality = 100) { + $image = $this->generate($mimeType, $quality); + + return 'data:' . $image['mimeType'] . ';base64,' . base64_encode($image['data']); + } + + // + // Forces the image to be downloaded to the clients machine. Must be called before any output is + // sent to the screen. + // + // $filename* (string) - The filename (without path) to send to the client (e.g. 'image.jpeg'). + // $mimeType (string) - The image format to output as a mime type (defaults to the original mime + // type). + // $quality (int) - Image quality as a percentage (default 100). + // + public function toDownload($filename, $mimeType = null, $quality = 100) { + $image = $this->generate($mimeType, $quality); + + // Set download headers + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Content-Description: File Transfer'); + header('Content-Length: ' . strlen($image['data'])); + header('Content-Transfer-Encoding: Binary'); + header('Content-Type: application/octet-stream'); + header("Content-Disposition: attachment; filename=\"$filename\""); + + echo $image['data']; + + return $this; + } + + // + // Writes the image to a file. + // + // $mimeType (string) - The image format to output as a mime type (defaults to the original mime + // type). + // $quality (int) - Image quality as a percentage (default 100). + // + // Returns a SimpleImage object. + // + public function toFile($file, $mimeType = null, $quality = 100) { + $image = $this->generate($mimeType, $quality); + + // Save the image to file + if(!file_put_contents($file, $image['data'])) { + throw new \Exception("Failed to write image to file: $file", self::ERR_WRITE); + } + + return $this; + } + + // + // Outputs the image to the screen. Must be called before any output is sent to the screen. + // + // $mimeType (string) - The image format to output as a mime type (defaults to the original mime + // type). + // $quality (int) - Image quality as a percentage (default 100). + // + // Returns a SimpleImage object. + // + public function toScreen($mimeType = null, $quality = 100) { + $image = $this->generate($mimeType, $quality); + + // Output the image to stdout + header('Content-Type: ' . $image['mimeType']); + echo $image['data']; + + return $this; + } + + // + // Generates an image string. + // + // $mimeType (string) - The image format to output as a mime type (defaults to the original mime + // type). + // $quality (int) - Image quality as a percentage (default 100). + // + // Returns a SimpleImage object. + // + public function toString($mimeType = null, $quality = 100) { + return $this->generate($mimeType, $quality)['data']; + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Utilities + ////////////////////////////////////////////////////////////////////////////////////////////////// + + // + // Ensures a numeric value is always within the min and max range. + // + // $value* (int|float) - A numeric value to test. + // $min* (int|float) - The minimum allowed value. + // $max* (int|float) - The maximum allowed value. + // + // Returns an int|float value. + // + private static function keepWithin($value, $min, $max) { + if($value < $min) return $min; + if($value > $max) return $max; + return $value; + } + + // + // Gets the image's current aspect ratio. + // + // Returns the aspect ratio as a float. + // + public function getAspectRatio() { + return $this->getWidth() / $this->getHeight(); + } + + // + // Gets the image's exif data. + // + // Returns an array of exif data or null if no data is available. + // + public function getExif() { + return isset($this->exif) ? $this->exif : null; + } + + // + // Gets the image's current height. + // + // Returns the height as an integer. + // + public function getHeight() { + return (int) imagesy($this->image); + } + + // + // Gets the mime type of the loaded image. + // + // Returns a mime type string. + // + public function getMimeType() { + return $this->mimeType; + } + + // + // Gets the image's current orientation. + // + // Returns a string: 'landscape', 'portrait', or 'square' + // + public function getOrientation() { + $width = $this->getWidth(); + $height = $this->getHeight(); + + if($width > $height) return 'landscape'; + if($width < $height) return 'portrait'; + return 'square'; + } + + // + // Gets the image's current width. + // + // Returns the width as an integer. + // + public function getWidth() { + return (int) imagesx($this->image); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Manipulation + ////////////////////////////////////////////////////////////////////////////////////////////////// + + // + // Same as PHP's imagecopymerge, but works with transparent images. Used internally for overlay. + // + private static function imageCopyMergeAlpha($dstIm, $srcIm, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH, $pct) { + // Are we merging with transparency? + if($pct < 100) { + // Disable alpha blending and "colorize" the image using a transparent color + imagealphablending($srcIm, false); + imagefilter($srcIm, IMG_FILTER_COLORIZE, 0, 0, 0, 127 * ((100 - $pct) / 100)); + } + + imagecopy($dstIm, $srcIm, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH); + + return true; + } + + // + // Rotates an image so the orientation will be correct based on its exif data. It is safe to call + // this method on images that don't have exif data (no changes will be made). + // + // Returns a SimpleImage object. + // + public function autoOrient() { + $exif = $this->getExif(); + + if(!$exif || !isset($exif['Orientation'])){ + return $this; + } + + switch($exif['Orientation']) { + case 1: // Do nothing! + break; + case 2: // Flip horizontally + $this->flip('x'); + break; + case 3: // Rotate 180 degrees + $this->rotate(180); + break; + case 4: // Flip vertically + $this->flip('y'); + break; + case 5: // Rotate 90 degrees clockwise and flip vertically + $this->flip('y')->rotate(90); + break; + case 6: // Rotate 90 clockwise + $this->rotate(90); + break; + case 7: // Rotate 90 clockwise and flip horizontally + $this->flip('x')->rotate(90); + break; + case 8: // Rotate 90 counterclockwise + $this->rotate(-90); + break; + } + + return $this; + } + + // + // Proportionally resize the image to fit inside a specific width and height. + // + // $maxWidth* (int) - The maximum width the image can be. + // $maxHeight* (int) - The maximum height the image can be. + // + // Returns a SimpleImage object. + // + public function bestFit($maxWidth, $maxHeight) { + // If the image already fits, there's nothing to do + if($this->getWidth() <= $maxWidth && $this->getHeight() <= $maxHeight) { + return $this; + } + + // Calculate max width or height based on orientation + if($this->getOrientation() === 'portrait') { + $height = $maxHeight; + $width = $maxHeight * $this->getAspectRatio(); + } else { + $width = $maxWidth; + $height = $maxWidth / $this->getAspectRatio(); + } + + // Reduce to max width + if($width > $maxWidth) { + $width = $maxWidth; + $height = $width / $this->getAspectRatio(); + } + + // Reduce to max height + if($height > $maxHeight) { + $height = $maxHeight; + $width = $height * $this->getAspectRatio(); + } + + return $this->resize($width, $height); + } + + // + // Crop the image. + // + // $x1 - Top left x coordinate. + // $y1 - Top left y coordinate. + // $x2 - Bottom right x coordinate. + // $y2 - Bottom right x coordinate. + // + // Returns a SimpleImage object. + // + public function crop($x1, $y1, $x2, $y2) { + // Keep crop within image dimensions + $x1 = self::keepWithin($x1, 0, $this->getWidth()); + $x2 = self::keepWithin($x2, 0, $this->getWidth()); + $y1 = self::keepWithin($y1, 0, $this->getHeight()); + $y2 = self::keepWithin($y2, 0, $this->getHeight()); + + // Crop it + $this->image = imagecrop($this->image, [ + 'x' => min($x1, $x2), + 'y' => min($y1, $y2), + 'width' => abs($x2 - $x1), + 'height' => abs($y2 - $y1) + ]); + + return $this; + } + + // + // Applies a duotone filter to the image. + // + // $lightColor* (string|array) - The lightest color in the duotone. + // $darkColor* (string|array) - The darkest color in the duotone. + // + // Returns a SimpleImage object. + // + function duotone($lightColor, $darkColor) { + $lightColor = self::normalizeColor($lightColor); + $darkColor = self::normalizeColor($darkColor); + + // Calculate averages between light and dark colors + $redAvg = $lightColor['red'] - $darkColor['red']; + $greenAvg = $lightColor['green'] - $darkColor['green']; + $blueAvg = $lightColor['blue'] - $darkColor['blue']; + + // Create a matrix of all possible duotone colors based on gray values + $pixels = []; + for($i = 0; $i <= 255; $i++) { + $grayAvg = $i / 255; + $pixels['red'][$i] = $darkColor['red'] + $grayAvg * $redAvg; + $pixels['green'][$i] = $darkColor['green'] + $grayAvg * $greenAvg; + $pixels['blue'][$i] = $darkColor['blue'] + $grayAvg * $blueAvg; + } + + // Apply the filter pixel by pixel + for($x = 0; $x < $this->getWidth(); $x++) { + for($y = 0; $y < $this->getHeight(); $y++) { + $rgb = $this->getColorAt($x, $y); + $gray = min(255, round(0.299 * $rgb['red'] + 0.114 * $rgb['blue'] + 0.587 * $rgb['green'])); + $this->dot($x, $y, [ + 'red' => $pixels['red'][$gray], + 'green' => $pixels['green'][$gray], + 'blue' => $pixels['blue'][$gray] + ]); + } + } + + return $this; + } + + // + // Proportionally resize the image to a specific height. + // + // **DEPRECATED:** This method was deprecated in version 3.2.2 and will be removed in version 4.0. + // Please use `resize(null, $height)` instead. + // + // $height* (int) - The height to resize the image to. + // + // Returns a SimpleImage object. + // + public function fitToHeight($height) { + return $this->resize(null, $height); + } + + // + // Proportionally resize the image to a specific width. + // + // **DEPRECATED:** This method was deprecated in version 3.2.2 and will be removed in version 4.0. + // Please use `resize($width, null)` instead. + // + // $width* (int) - The width to resize the image to. + // + // Returns a SimpleImage object. + // + public function fitToWidth($width) { + return $this->resize($width, null); + } + + // + // Flip the image horizontally or vertically. + // + // $direction* (string) - The direction to flip: x|y|both + // + // Returns a SimpleImage object. + // + public function flip($direction) { + switch($direction) { + case 'x': + imageflip($this->image, IMG_FLIP_HORIZONTAL); + break; + case 'y': + imageflip($this->image, IMG_FLIP_VERTICAL); + break; + case 'both': + imageflip($this->image, IMG_FLIP_BOTH); + break; + } + + return $this; + } + + // + // Reduces the image to a maximum number of colors. + // + // $max* (int) - The maximum number of colors to use. + // $dither (bool) - Whether or not to use a dithering effect (default true). + // + // Returns a SimpleImage object. + // + public function maxColors($max, $dither = true) { + imagetruecolortopalette($this->image, $dither, max(1, $max)); + + return $this; + } + + // + // Place an image on top of the current image. + // + // $overlay* (string|SimpleImage) - The image to overlay. This can be a filename, a data URI, or + // a SimpleImage object. + // $anchor (string) - The anchor point: 'center', 'top', 'bottom', 'left', 'right', 'top left', + // 'top right', 'bottom left', 'bottom right' (default 'center') + // $opacity (float) - The opacity level of the overlay 0-1 (default 1). + // $xOffset (int) - Horizontal offset in pixels (default 0). + // $yOffset (int) - Vertical offset in pixels (default 0). + // + // Returns a SimpleImage object. + // + public function overlay($overlay, $anchor = 'center', $opacity = 1, $xOffset = 0, $yOffset = 0) { + // Load overlay image + if(!($overlay instanceof SimpleImage)) { + $overlay = new SimpleImage($overlay); + } + + // Convert opacity + $opacity = self::keepWithin($opacity, 0, 1) * 100; + + // Determine placement + switch($anchor) { + case 'top left': + $x = $xOffset; + $y = $yOffset; + break; + case 'top right': + $x = $this->getWidth() - $overlay->getWidth() + $xOffset; + $y = $yOffset; + break; + case 'top': + $x = ($this->getWidth() / 2) - ($overlay->getWidth() / 2) + $xOffset; + $y = $yOffset; + break; + case 'bottom left': + $x = $xOffset; + $y = $this->getHeight() - $overlay->getHeight() + $yOffset; + break; + case 'bottom right': + $x = $this->getWidth() - $overlay->getWidth() + $xOffset; + $y = $this->getHeight() - $overlay->getHeight() + $yOffset; + break; + case 'bottom': + $x = ($this->getWidth() / 2) - ($overlay->getWidth() / 2) + $xOffset; + $y = $this->getHeight() - $overlay->getHeight() + $yOffset; + break; + case 'left': + $x = $xOffset; + $y = ($this->getHeight() / 2) - ($overlay->getHeight() / 2) + $yOffset; + break; + case 'right': + $x = $this->getWidth() - $overlay->getWidth() + $xOffset; + $y = ($this->getHeight() / 2) - ($overlay->getHeight() / 2) + $yOffset; + break; + default: + $x = ($this->getWidth() / 2) - ($overlay->getWidth() / 2) + $xOffset; + $y = ($this->getHeight() / 2) - ($overlay->getHeight() / 2) + $yOffset; + break; + } + + // Perform the overlay + self::imageCopyMergeAlpha( + $this->image, + $overlay->image, + $x, $y, + 0, 0, + $overlay->getWidth(), + $overlay->getHeight(), + $opacity + ); + + return $this; + } + + // + // Resize an image to the specified dimensions. If only one dimension is specified, the image will + // be resized proportionally. + // + // $width* (int) - The new image width. + // $height* (int) - The new image height. + // + // Returns a SimpleImage object. + // + public function resize($width = null, $height = null) { + // No dimentions specified + if(!$width && !$height) { + return $this; + } + + // Resize to width + if($width && !$height) { + $height = $width / $this->getAspectRatio(); + } + + // Resize to height + if(!$width && $height) { + $width = $height * $this->getAspectRatio(); + } + + // If the dimensions are the same, there's no need to resize + if($this->getWidth() === $width && $this->getHeight() === $height) { + return $this; + } + + // We can't use imagescale because it doesn't seem to preserve transparency properly. The + // workaround is to create a new truecolor image, allocate a transparent color, and copy the + // image over to it using imagecopyresampled. + $newImage = imagecreatetruecolor($width, $height); + $transparentColor = imagecolorallocatealpha($newImage, 0, 0, 0, 127); + imagecolortransparent($newImage, $transparentColor); + imagefill($newImage, 0, 0, $transparentColor); + imagecopyresampled( + $newImage, + $this->image, + 0, 0, 0, 0, + $width, + $height, + $this->getWidth(), + $this->getHeight() + ); + + // Swap out the new image + $this->image = $newImage; + + return $this; + } + + // + // Rotates the image. + // + // $angle* (int) - The angle of rotation (-360 - 360). + // $backgroundColor (string|array) - The background color to use for the uncovered zone area + // after rotation (default 'transparent'). + // + // Returns a SimpleImage object. + // + public function rotate($angle, $backgroundColor = 'transparent') { + // Rotate the image on a canvas with the desired background color + $backgroundColor = $this->allocateColor($backgroundColor); + + $this->image = imagerotate( + $this->image, + -(self::keepWithin($angle, -360, 360)), + $backgroundColor + ); + + return $this; + } + + // + // Adds text to the image. + // + // $text* (string) - The desired text. + // $options (array) - An array of options. + // - fontFile* (string) - The TrueType (or compatible) font file to use. + // - size (int) - The size of the font in pixels (default 12). + // - color (string|array) - The text color (default black). + // - anchor (string) - The anchor point: 'center', 'top', 'bottom', 'left', 'right', + // 'top left', 'top right', 'bottom left', 'bottom right' (default 'center'). + // - xOffset (int) - The horizontal offset in pixels (default 0). + // - yOffset (int) - The vertical offset in pixels (default 0). + // - shadow (array) - Text shadow params. + // - x* (int) - Horizontal offset in pixels. + // - y* (int) - Vertical offset in pixels. + // - color* (string|array) - The text shadow color. + // $boundary (array) - If passed, this variable will contain an array with coordinates that + // surround the text: [x1, y1, x2, y2, width, height]. This can be used for calculating the + // text's position after it gets added to the image. + // + // Returns a SimpleImage object. + // + public function text($text, $options, &$boundary = null) { + // Check for freetype support + if(!function_exists('imagettftext')) { + throw new \Exception( + 'Freetype support is not enabled in your version of PHP.', + self::ERR_FREETYPE_NOT_ENABLED + ); + } + + // Default options + $options = array_merge([ + 'fontFile' => null, + 'size' => 12, + 'color' => 'black', + 'anchor' => 'center', + 'xOffset' => 0, + 'yOffset' => 0, + 'shadow' => null + ], $options); + + // Extract and normalize options + $fontFile = $options['fontFile']; + $size = ($options['size'] / 96) * 72; // Convert px to pt (72pt per inch, 96px per inch) + $color = $this->allocateColor($options['color']); + $anchor = $options['anchor']; + $xOffset = $options['xOffset']; + $yOffset = $options['yOffset']; + $angle = 0; + + // Calculate the bounding box dimensions + // + // Since imagettfbox() returns a bounding box from the text's baseline, we can end up with + // different heights for different strings of the same font size. For example, 'type' will often + // be taller than 'text' because the former has a descending letter. + // + // To compensate for this, we create two bounding boxes: one to measure the cap height and + // another to measure the descender height. Based on that, we can adjust the text vertically + // to appear inside the box with a reasonable amount of consistency. + // + // See: https://github.com/claviska/SimpleImage/issues/165 + // + $box = imagettfbbox($size, $angle, $fontFile, $text); + if(!$box) { + throw new \Exception("Unable to load font file: $fontFile", self::ERR_FONT_FILE); + } + $boxWidth = abs($box[6] - $box[2]); + $boxHeight = $options['size']; + + // Determine cap height + $box = imagettfbbox($size, $angle, $fontFile, 'X'); + $capHeight = abs($box[7] - $box[1]); + + // Determine descender height + $box = imagettfbbox($size, $angle, $fontFile, 'X Qgjpqy'); + $fullHeight = abs($box[7] - $box[1]); + $descenderHeight = $fullHeight - $capHeight; + + // Determine position + switch($anchor) { + case 'top left': + $x = $xOffset; + $y = $yOffset + $boxHeight; + break; + case 'top right': + $x = $this->getWidth() - $boxWidth + $xOffset; + $y = $yOffset + $boxHeight; + break; + case 'top': + $x = ($this->getWidth() / 2) - ($boxWidth / 2) + $xOffset; + $y = $yOffset + $boxHeight; + break; + case 'bottom left': + $x = $xOffset; + $y = $this->getHeight() - $boxHeight + $yOffset + $boxHeight; + break; + case 'bottom right': + $x = $this->getWidth() - $boxWidth + $xOffset; + $y = $this->getHeight() - $boxHeight + $yOffset + $boxHeight; + break; + case 'bottom': + $x = ($this->getWidth() / 2) - ($boxWidth / 2) + $xOffset; + $y = $this->getHeight() - $boxHeight + $yOffset + $boxHeight; + break; + case 'left': + $x = $xOffset; + $y = ($this->getHeight() / 2) - (($boxHeight / 2) - $boxHeight) + $yOffset; + break; + case 'right'; + $x = $this->getWidth() - $boxWidth + $xOffset; + $y = ($this->getHeight() / 2) - (($boxHeight / 2) - $boxHeight) + $yOffset; + break; + default: // center + $x = ($this->getWidth() / 2) - ($boxWidth / 2) + $xOffset; + $y = ($this->getHeight() / 2) - (($boxHeight / 2) - $boxHeight) + $yOffset; + break; + } + + $x = (int) round($x); + $y = (int) round($y); + + // Pass the boundary back by reference + $boundary = [ + 'x1' => $x, + 'y1' => $y - $boxHeight, // $y is the baseline, not the top! + 'x2' => $x + $boxWidth, + 'y2' => $y, + 'width' => $boxWidth, + 'height' => $boxHeight + ]; + + // Text shadow + if(is_array($options['shadow'])) { + imagettftext( + $this->image, + $size, + $angle, + $x + $options['shadow']['x'], + $y + $options['shadow']['y'] - $descenderHeight, + $this->allocateColor($options['shadow']['color']), + $fontFile, + $text + ); + } + + // Draw the text + imagettftext($this->image, $size, $angle, $x, $y - $descenderHeight, $color, $fontFile, $text); + + return $this; + } + + // + // Creates a thumbnail image. This function attempts to get the image as close to the provided + // dimensions as possible, then crops the remaining overflow to force the desired size. Useful + // for generating thumbnail images. + // + // $width* (int) - The thumbnail width. + // $height* (int) - The thumbnail height. + // $anchor (string) - The anchor point: 'center', 'top', 'bottom', 'left', 'right', 'top left', + // 'top right', 'bottom left', 'bottom right' (default 'center'). + // + // Returns a SimpleImage object. + // + public function thumbnail($width, $height, $anchor = 'center') { + // Determine aspect ratios + $currentRatio = $this->getHeight() / $this->getWidth(); + $targetRatio = $height / $width; + + // Fit to height/width + if($targetRatio > $currentRatio) { + $this->resize(null, $height); + } else { + $this->resize($width, null); + } + + switch($anchor) { + case 'top': + $x1 = floor(($this->getWidth() / 2) - ($width / 2)); + $x2 = $width + $x1; + $y1 = 0; + $y2 = $height; + break; + case 'bottom': + $x1 = floor(($this->getWidth() / 2) - ($width / 2)); + $x2 = $width + $x1; + $y1 = $this->getHeight() - $height; + $y2 = $this->getHeight(); + break; + case 'left': + $x1 = 0; + $x2 = $width; + $y1 = floor(($this->getHeight() / 2) - ($height / 2)); + $y2 = $height + $y1; + break; + case 'right': + $x1 = $this->getWidth() - $width; + $x2 = $this->getWidth(); + $y1 = floor(($this->getHeight() / 2) - ($height / 2)); + $y2 = $height + $y1; + break; + case 'top left': + $x1 = 0; + $x2 = $width; + $y1 = 0; + $y2 = $height; + break; + case 'top right': + $x1 = $this->getWidth() - $width; + $x2 = $this->getWidth(); + $y1 = 0; + $y2 = $height; + break; + case 'bottom left': + $x1 = 0; + $x2 = $width; + $y1 = $this->getHeight() - $height; + $y2 = $this->getHeight(); + break; + case 'bottom right': + $x1 = $this->getWidth() - $width; + $x2 = $this->getWidth(); + $y1 = $this->getHeight() - $height; + $y2 = $this->getHeight(); + break; + default: + $x1 = floor(($this->getWidth() / 2) - ($width / 2)); + $x2 = $width + $x1; + $y1 = floor(($this->getHeight() / 2) - ($height / 2)); + $y2 = $height + $y1; + break; + } + + // Return the cropped thumbnail image + return $this->crop($x1, $y1, $x2, $y2); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Drawing + ////////////////////////////////////////////////////////////////////////////////////////////////// + + // + // Draws an arc. + // + // $x* (int) - The x coordinate of the arc's center. + // $y* (int) - The y coordinate of the arc's center. + // $width* (int) - The width of the arc. + // $height* (int) - The height of the arc. + // $start* (int) - The start of the arc in degrees. + // $end* (int) - The end of the arc in degrees. + // $color* (string|array) - The arc color. + // $thickness (int|string) - Line thickness in pixels or 'filled' (default 1). + // + // Returns a SimpleImage object. + // + public function arc($x, $y, $width, $height, $start, $end, $color, $thickness = 1) { + // Allocate the color + $color = $this->allocateColor($color); + + // Draw an arc + if($thickness === 'filled') { + imagesetthickness($this->image, 1); + imagefilledarc($this->image, $x, $y, $width, $height, $start, $end, $color, IMG_ARC_PIE); + } else { + imagesetthickness($this->image, $thickness); + imagearc($this->image, $x, $y, $width, $height, $start, $end, $color); + } + + return $this; + } + + // + // Draws a border around the image. + // + // $color* (string|array) - The border color. + // $thickness (int) - The thickness of the border (default 1). + // + // Returns a SimpleImage object. + // + public function border($color, $thickness = 1) { + $x1 = 0; + $y1 = 0; + $x2 = $this->getWidth() - 1; + $y2 = $this->getHeight() - 1; + + // Draw a border rectangle until it reaches the correct width + for($i = 0; $i < $thickness; $i++) { + $this->rectangle($x1++, $y1++, $x2--, $y2--, $color); + } + + return $this; + } + + // + // Draws a single pixel dot. + // + // $x* (int) - The x coordinate of the dot. + // $y* (int) - The y coordinate of the dot. + // $color* (string|array) - The dot color. + // + // Returns a SimpleImage object. + // + public function dot($x, $y, $color) { + $color = $this->allocateColor($color); + imagesetpixel($this->image, $x, $y, $color); + + return $this; + } + + // + // Draws an ellipse. + // + // $x* (int) - The x coordinate of the center. + // $y* (int) - The y coordinate of the center. + // $width* (int) - The ellipse width. + // $height* (int) - The ellipse height. + // $color* (string|array) - The ellipse color. + // $thickness (int|string) - Line thickness in pixels or 'filled' (default 1). + // + // Returns a SimpleImage object. + // + public function ellipse($x, $y, $width, $height, $color, $thickness = 1) { + // Allocate the color + $color = $this->allocateColor($color); + + // Draw an ellipse + if($thickness === 'filled') { + imagesetthickness($this->image, 1); + imagefilledellipse($this->image, $x, $y, $width, $height, $color); + } else { + // imagesetthickness doesn't appear to work with imageellipse, so we work around it. + imagesetthickness($this->image, 1); + $i = 0; + while($i++ < $thickness * 2 - 1) { + imageellipse($this->image, $x, $y, --$width, $height--, $color); + } + } + + return $this; + } + + // + // Fills the image with a solid color. + // + // $color (string|array) - The fill color. + // + // Returns a SimpleImage object. + // + public function fill($color) { + // Draw a filled rectangle over the entire image + $this->rectangle(0, 0, $this->getWidth(), $this->getHeight(), 'white', 'filled'); + + // Now flood it with the appropriate color + $color = $this->allocateColor($color); + imagefill($this->image, 0, 0, $color); + + return $this; + } + + // + // Draws a line. + // + // $x1* (int) - The x coordinate for the first point. + // $y1* (int) - The y coordinate for the first point. + // $x2* (int) - The x coordinate for the second point. + // $y2* (int) - The y coordinate for the second point. + // $color (string|array) - The line color. + // $thickness (int) - The line thickness (default 1). + // + // Returns a SimpleImage object. + // + public function line($x1, $y1, $x2, $y2, $color, $thickness = 1) { + // Allocate the color + $color = $this->allocateColor($color); + + // Draw a line + imagesetthickness($this->image, $thickness); + imageline($this->image, $x1, $y1, $x2, $y2, $color); + + return $this; + } + + // + // Draws a polygon. + // + // $vertices* (array) - The polygon's vertices in an array of x/y arrays. Example: + // [ + // ['x' => x1, 'y' => y1], + // ['x' => x2, 'y' => y2], + // ['x' => xN, 'y' => yN] + // ] + // $color* (string|array) - The polygon color. + // $thickness (int|string) - Line thickness in pixels or 'filled' (default 1). + // + // Returns a SimpleImage object. + // + public function polygon($vertices, $color, $thickness = 1) { + // Allocate the color + $color = $this->allocateColor($color); + + // Convert [['x' => x1, 'y' => x1], ['x' => x1, 'y' => y2], ...] to [x1, y1, x2, y2, ...] + $points = []; + foreach($vertices as $vals) { + $points[] = $vals['x']; + $points[] = $vals['y']; + } + + // Draw a polygon + if($thickness === 'filled') { + imagesetthickness($this->image, 1); + imagefilledpolygon($this->image, $points, count($vertices), $color); + } else { + imagesetthickness($this->image, $thickness); + imagepolygon($this->image, $points, count($vertices), $color); + } + + return $this; + } + + // + // Draws a rectangle. + // + // $x1* (int) - The upper left x coordinate. + // $y1* (int) - The upper left y coordinate. + // $x2* (int) - The bottom right x coordinate. + // $y2* (int) - The bottom right y coordinate. + // $color* (string|array) - The rectangle color. + // $thickness (int|string) - Line thickness in pixels or 'filled' (default 1). + // + // Returns a SimpleImage object. + // + public function rectangle($x1, $y1, $x2, $y2, $color, $thickness = 1) { + // Allocate the color + $color = $this->allocateColor($color); + + // Draw a rectangle + if($thickness === 'filled') { + imagesetthickness($this->image, 1); + imagefilledrectangle($this->image, $x1, $y1, $x2, $y2, $color); + } else { + imagesetthickness($this->image, $thickness); + imagerectangle($this->image, $x1, $y1, $x2, $y2, $color); + } + + return $this; + } + + // + // Draws a rounded rectangle. + // + // $x1* (int) - The upper left x coordinate. + // $y1* (int) - The upper left y coordinate. + // $x2* (int) - The bottom right x coordinate. + // $y2* (int) - The bottom right y coordinate. + // $radius* (int) - The border radius in pixels. + // $color* (string|array) - The rectangle color. + // $thickness (int|string) - Line thickness in pixels or 'filled' (default 1). + // + // Returns a SimpleImage object. + // + public function roundedRectangle($x1, $y1, $x2, $y2, $radius, $color, $thickness = 1) { + if($thickness === 'filled') { + // Draw the filled rectangle without edges + $this->rectangle($x1 + $radius + 1, $y1, $x2 - $radius - 1, $y2, $color, 'filled'); + $this->rectangle($x1, $y1 + $radius + 1, $x1 + $radius, $y2 - $radius - 1, $color, 'filled'); + $this->rectangle($x2 - $radius, $y1 + $radius + 1, $x2, $y2 - $radius - 1, $color, 'filled'); + // Fill in the edges with arcs + $this->arc($x1 + $radius, $y1 + $radius, $radius * 2, $radius * 2, 180, 270, $color, 'filled'); + $this->arc($x2 - $radius, $y1 + $radius, $radius * 2, $radius * 2, 270, 360, $color, 'filled'); + $this->arc($x1 + $radius, $y2 - $radius, $radius * 2, $radius * 2, 90, 180, $color, 'filled'); + $this->arc($x2 - $radius, $y2 - $radius, $radius * 2, $radius * 2, 360, 90, $color, 'filled'); + } else { + // Draw the rectangle outline without edges + $this->line($x1 + $radius, $y1, $x2 - $radius, $y1, $color, $thickness); + $this->line($x1 + $radius, $y2, $x2 - $radius, $y2, $color, $thickness); + $this->line($x1, $y1 + $radius, $x1, $y2 - $radius, $color, $thickness); + $this->line($x2, $y1 + $radius, $x2, $y2 - $radius, $color, $thickness); + // Fill in the edges with arcs + $this->arc($x1 + $radius, $y1 + $radius, $radius * 2, $radius * 2, 180, 270, $color, $thickness); + $this->arc($x2 - $radius, $y1 + $radius, $radius * 2, $radius * 2, 270, 360, $color, $thickness); + $this->arc($x1 + $radius, $y2 - $radius, $radius * 2, $radius * 2, 90, 180, $color, $thickness); + $this->arc($x2 - $radius, $y2 - $radius, $radius * 2, $radius * 2, 360, 90, $color, $thickness); + } + + return $this; + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Filters + ////////////////////////////////////////////////////////////////////////////////////////////////// + + // + // Applies the blur filter. + // + // $type (string) - The blur algorithm to use: 'selective', 'gaussian' (default 'gaussian'). + // $passes (int) - The number of time to apply the filter, enhancing the effect (default 1). + // + // Returns a SimpleImage object. + // + public function blur($type = 'selective', $passes = 1) { + $filter = $type === 'gaussian' ? IMG_FILTER_GAUSSIAN_BLUR : IMG_FILTER_SELECTIVE_BLUR; + + for($i = 0; $i < $passes; $i++) { + imagefilter($this->image, $filter); + } + + return $this; + } + + // + // Applies the brightness filter to brighten the image. + // + // $percentage* (int) - Percentage to brighten the image (0 - 100). + // + // Returns a SimpleImage object. + // + public function brighten($percentage) { + $percentage = self::keepWithin(255 * $percentage / 100, 0, 255); + + imagefilter($this->image, IMG_FILTER_BRIGHTNESS, $percentage); + + return $this; + } + + // + // Applies the colorize filter. + // + // $color* (string|array) - The filter color. + // + // Returns a SimpleImage object. + // + public function colorize($color) { + $color = self::normalizeColor($color); + + imagefilter( + $this->image, + IMG_FILTER_COLORIZE, + $color['red'], + $color['green'], + $color['blue'], + 127 - ($color['alpha'] * 127) + ); + + return $this; + } + + // + // Applies the contrast filter. + // + // $percentage* (int) - Percentage to adjust (-100 - 100). + // + // Returns a SimpleImage object. + // + public function contrast($percentage) { + imagefilter($this->image, IMG_FILTER_CONTRAST, self::keepWithin($percentage, -100, 100)); + + return $this; + } + + // + // Applies the brightness filter to darken the image. + // + // $percentage* (int) - Percentage to darken the image (0 - 100). + // + // Returns a SimpleImage object. + // + public function darken($percentage) { + $percentage = self::keepWithin(255 * $percentage / 100, 0, 255); + + imagefilter($this->image, IMG_FILTER_BRIGHTNESS, -$percentage); + + return $this; + } + + // + // Applies the desaturate (grayscale) filter. + // + // Returns a SimpleImage object. + // + public function desaturate() { + imagefilter($this->image, IMG_FILTER_GRAYSCALE); + + return $this; + } + + // + // Applies the edge detect filter. + // + // Returns a SimpleImage object. + // + public function edgeDetect() { + imagefilter($this->image, IMG_FILTER_EDGEDETECT); + + return $this; + } + + // + // Applies the emboss filter. + // + // Returns a SimpleImage object. + // + public function emboss() { + imagefilter($this->image, IMG_FILTER_EMBOSS); + + return $this; + } + + // + // Inverts the image's colors. + // + // Returns a SimpleImage object. + // + public function invert() { + imagefilter($this->image, IMG_FILTER_NEGATE); + + return $this; + } + + // + // Changes the image's opacity level. + // + // $opacity* (float) - The desired opacity level (0 - 1). + // + // Returns a SimpleImage object. + // + public function opacity($opacity) { + // Create a transparent image + $newImage = new SimpleImage(); + $newImage->fromNew($this->getWidth(), $this->getHeight()); + + // Copy the current image (with opacity) onto the transparent image + self::imageCopyMergeAlpha( + $newImage->image, + $this->image, + $x, $y, + 0, 0, + $this->getWidth(), + $this->getHeight(), + self::keepWithin($opacity, 0, 1) * 100 + ); + + return $this; + } + + // + // Applies the pixelate filter. + // + // $size (int) - The size of the blocks in pixels (default 10). + // + // Returns a SimpleImage object. + // + public function pixelate($size = 10) { + imagefilter($this->image, IMG_FILTER_PIXELATE, $size, true); + + return $this; + } + + // + // Simulates a sepia effect by desaturating the image and applying a sepia tone. + // + // Returns a SimpleImage object. + // + public function sepia() { + imagefilter($this->image, IMG_FILTER_GRAYSCALE); + imagefilter($this->image, IMG_FILTER_COLORIZE, 70, 35, 0); + + return $this; + } + + // + // Sharpens the image. + // + // Returns a SimpleImage object. + // + public function sharpen() { + $sharpen = [ + [0, -1, 0], + [-1, 5, -1], + [0, -1, 0] + ]; + $divisor = array_sum(array_map('array_sum', $sharpen)); + + imageconvolution($this->image, $sharpen, $divisor, 0); + + return $this; + } + + // + // Applies the mean remove filter to produce a sketch effect. + // + // Returns a SimpleImage object. + // + public function sketch() { + imagefilter($this->image, IMG_FILTER_MEAN_REMOVAL); + + return $this; + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Color utilities + ////////////////////////////////////////////////////////////////////////////////////////////////// + + // + // Converts a "friendly color" into a color identifier for use with GD's image functions. + // + // $image (resource) - The target image. + // $color (string|array) - The color to allocate. + // + // Returns a color identifier. + // + private function allocateColor($color) { + $color = self::normalizeColor($color); + + // Was this color already allocated? + $index = imagecolorexactalpha( + $this->image, + $color['red'], + $color['green'], + $color['blue'], + 127 - ($color['alpha'] * 127) + ); + if($index > -1) { + // Yes, return this color index + return $index; + } + + // Allocate a new color index + return imagecolorallocatealpha( + $this->image, + $color['red'], + $color['green'], + $color['blue'], + 127 - ($color['alpha'] * 127) + ); + } + + // + // Adjusts a color by increasing/decreasing red/green/blue/alpha values independently. + // + // $color* (string|array) - The color to adjust. + // $red* (int) - Red adjustment (-255 - 255). + // $green* (int) - Green adjustment (-255 - 255). + // $blue* (int) - Blue adjustment (-255 - 255). + // $alpha* (float) - Alpha adjustment (-1 - 1). + // + // Returns an RGBA color array. + // + public static function adjustColor($color, $red, $green, $blue, $alpha) { + // Normalize to RGBA + $color = self::normalizeColor($color); + + // Adjust each channel + return self::normalizeColor([ + 'red' => $color['red'] + $red, + 'green' => $color['green'] + $green, + 'blue' => $color['blue'] + $blue, + 'alpha' => $color['alpha'] + $alpha + ]); + } + + // + // Darkens a color. + // + // $color* (string|array) - The color to darken. + // $amount* (int) - Amount to darken (0 - 255). + // + // Returns an RGBA color array. + // + public static function darkenColor($color, $amount) { + return self::adjustColor($color, -$amount, -$amount, -$amount, 0); + } + + // + // Extracts colors from an image like a human would do.™ This method requires the third-party + // library \League\ColorExtractor. If you're using Composer, it will be installed for you + // automatically. + // + // $count (int) - The max number of colors to extract (default 5). + // $backgroundColor (string|array) - By default any pixel with alpha value greater than zero will + // be discarded. This is because transparent colors are not perceived as is. For example, fully + // transparent black would be seen white on a white background. So if you want to take + // transparency into account, you have to specify a default background color. + // + // Returns an array of RGBA colors arrays. + // + public function extractColors($count = 5, $backgroundColor = null) { + // Check for required library + if(!class_exists('\League\ColorExtractor\ColorExtractor')) { + throw new \Exception( + 'Required library \League\ColorExtractor is missing.', + self::ERR_LIB_NOT_LOADED + ); + } + + // Convert background color to an integer value + if($backgroundColor) { + $backgroundColor = self::normalizeColor($backgroundColor); + $backgroundColor = \League\ColorExtractor\Color::fromRgbToInt([ + 'r' => $backgroundColor['red'], + 'g' => $backgroundColor['green'], + 'b' => $backgroundColor['blue'] + ]); + } + + // Extract colors from the image + $palette = \League\ColorExtractor\Palette::fromGD($this->image, $backgroundColor); + $extractor = new \League\ColorExtractor\ColorExtractor($palette); + $colors = $extractor->extract($count); + + // Convert colors to an RGBA color array + foreach($colors as $key => $value) { + $colors[$key] = self::normalizeColor(\League\ColorExtractor\Color::fromIntToHex($value)); + } + + return $colors; + } + + // + // Gets the RGBA value of a single pixel. + // + // $x* (int) - The horizontal position of the pixel. + // $y* (int) - The vertical position of the pixel. + // + // Returns an RGBA color array or false if the x/y position is off the canvas. + // + public function getColorAt($x, $y) { + // Coordinates must be on the canvas + if($x < 0 || $x > $this->getWidth() || $y < 0 || $y > $this->getHeight()) { + return false; + } + + // Get the color of this pixel and convert it to RGBA + $color = imagecolorat($this->image, $x, $y); + $rgba = imagecolorsforindex($this->image, $color); + $rgba['alpha'] = 127 - ($color >> 24) & 0xFF; + + return $rgba; + } + + // + // Lightens a color. + // + // $color* (string|array) - The color to lighten. + // $amount* (int) - Amount to darken (0 - 255). + // + // Returns an RGBA color array. + // + public static function lightenColor($color, $amount) { + return self::adjustColor($color, $amount, $amount, $amount, 0); + } + + // + // Normalizes a hex or array color value to a well-formatted RGBA array. + // + // $color* (string|array) - A CSS color name, hex string, or an array [red, green, blue, alpha]. + // You can pipe alpha transparency through hex strings and color names. For example: + // + // #fff|0.50 <-- 50% white + // red|0.25 <-- 25% red + // + // Returns an array: [red, green, blue, alpha] + // + public static function normalizeColor($color) { + // 140 CSS color names and hex values + $cssColors = [ + 'aliceblue' => '#f0f8ff', 'antiquewhite' => '#faebd7', 'aqua' => '#00ffff', + 'aquamarine' => '#7fffd4', 'azure' => '#f0ffff', 'beige' => '#f5f5dc', 'bisque' => '#ffe4c4', + 'black' => '#000000', 'blanchedalmond' => '#ffebcd', 'blue' => '#0000ff', + 'blueviolet' => '#8a2be2', 'brown' => '#a52a2a', 'burlywood' => '#deb887', + 'cadetblue' => '#5f9ea0', 'chartreuse' => '#7fff00', 'chocolate' => '#d2691e', + 'coral' => '#ff7f50', 'cornflowerblue' => '#6495ed', 'cornsilk' => '#fff8dc', + 'crimson' => '#dc143c', 'cyan' => '#00ffff', 'darkblue' => '#00008b', 'darkcyan' => '#008b8b', + 'darkgoldenrod' => '#b8860b', 'darkgray' => '#a9a9a9', 'darkgrey' => '#a9a9a9', + 'darkgreen' => '#006400', 'darkkhaki' => '#bdb76b', 'darkmagenta' => '#8b008b', + 'darkolivegreen' => '#556b2f', 'darkorange' => '#ff8c00', 'darkorchid' => '#9932cc', + 'darkred' => '#8b0000', 'darksalmon' => '#e9967a', 'darkseagreen' => '#8fbc8f', + 'darkslateblue' => '#483d8b', 'darkslategray' => '#2f4f4f', 'darkslategrey' => '#2f4f4f', + 'darkturquoise' => '#00ced1', 'darkviolet' => '#9400d3', 'deeppink' => '#ff1493', + 'deepskyblue' => '#00bfff', 'dimgray' => '#696969', 'dimgrey' => '#696969', + 'dodgerblue' => '#1e90ff', 'firebrick' => '#b22222', 'floralwhite' => '#fffaf0', + 'forestgreen' => '#228b22', 'fuchsia' => '#ff00ff', 'gainsboro' => '#dcdcdc', + 'ghostwhite' => '#f8f8ff', 'gold' => '#ffd700', 'goldenrod' => '#daa520', 'gray' => '#808080', + 'grey' => '#808080', 'green' => '#008000', 'greenyellow' => '#adff2f', + 'honeydew' => '#f0fff0', 'hotpink' => '#ff69b4', 'indianred ' => '#cd5c5c', + 'indigo ' => '#4b0082', 'ivory' => '#fffff0', 'khaki' => '#f0e68c', 'lavender' => '#e6e6fa', + 'lavenderblush' => '#fff0f5', 'lawngreen' => '#7cfc00', 'lemonchiffon' => '#fffacd', + 'lightblue' => '#add8e6', 'lightcoral' => '#f08080', 'lightcyan' => '#e0ffff', + 'lightgoldenrodyellow' => '#fafad2', 'lightgray' => '#d3d3d3', 'lightgrey' => '#d3d3d3', + 'lightgreen' => '#90ee90', 'lightpink' => '#ffb6c1', 'lightsalmon' => '#ffa07a', + 'lightseagreen' => '#20b2aa', 'lightskyblue' => '#87cefa', 'lightslategray' => '#778899', + 'lightslategrey' => '#778899', 'lightsteelblue' => '#b0c4de', 'lightyellow' => '#ffffe0', + 'lime' => '#00ff00', 'limegreen' => '#32cd32', 'linen' => '#faf0e6', 'magenta' => '#ff00ff', + 'maroon' => '#800000', 'mediumaquamarine' => '#66cdaa', 'mediumblue' => '#0000cd', + 'mediumorchid' => '#ba55d3', 'mediumpurple' => '#9370db', 'mediumseagreen' => '#3cb371', + 'mediumslateblue' => '#7b68ee', 'mediumspringgreen' => '#00fa9a', + 'mediumturquoise' => '#48d1cc', 'mediumvioletred' => '#c71585', 'midnightblue' => '#191970', + 'mintcream' => '#f5fffa', 'mistyrose' => '#ffe4e1', 'moccasin' => '#ffe4b5', + 'navajowhite' => '#ffdead', 'navy' => '#000080', 'oldlace' => '#fdf5e6', 'olive' => '#808000', + 'olivedrab' => '#6b8e23', 'orange' => '#ffa500', 'orangered' => '#ff4500', + 'orchid' => '#da70d6', 'palegoldenrod' => '#eee8aa', 'palegreen' => '#98fb98', + 'paleturquoise' => '#afeeee', 'palevioletred' => '#db7093', 'papayawhip' => '#ffefd5', + 'peachpuff' => '#ffdab9', 'peru' => '#cd853f', 'pink' => '#ffc0cb', 'plum' => '#dda0dd', + 'powderblue' => '#b0e0e6', 'purple' => '#800080', 'rebeccapurple' => '#663399', + 'red' => '#ff0000', 'rosybrown' => '#bc8f8f', 'royalblue' => '#4169e1', + 'saddlebrown' => '#8b4513', 'salmon' => '#fa8072', 'sandybrown' => '#f4a460', + 'seagreen' => '#2e8b57', 'seashell' => '#fff5ee', 'sienna' => '#a0522d', + 'silver' => '#c0c0c0', 'skyblue' => '#87ceeb', 'slateblue' => '#6a5acd', + 'slategray' => '#708090', 'slategrey' => '#708090', 'snow' => '#fffafa', + 'springgreen' => '#00ff7f', 'steelblue' => '#4682b4', 'tan' => '#d2b48c', 'teal' => '#008080', + 'thistle' => '#d8bfd8', 'tomato' => '#ff6347', 'turquoise' => '#40e0d0', + 'violet' => '#ee82ee', 'wheat' => '#f5deb3', 'white' => '#ffffff', 'whitesmoke' => '#f5f5f5', + 'yellow' => '#ffff00', 'yellowgreen' => '#9acd32' + ]; + + // Parse alpha from '#fff|.5' and 'white|.5' + if(is_string($color) && strstr($color, '|')) { + $color = explode('|', $color); + $alpha = (float) $color[1]; + $color = trim($color[0]); + } else { + $alpha = 1; + } + + // Translate CSS color names to hex values + if(is_string($color) && array_key_exists(strtolower($color), $cssColors)) { + $color = $cssColors[strtolower($color)]; + } + + // Translate transparent keyword to a transparent color + if($color === 'transparent') { + $color = ['red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 0]; + } + + // Convert hex values to RGBA + if(is_string($color)) { + // Remove # + $hex = preg_replace('/^#/', '', $color); + + // Support short and standard hex codes + if(strlen($hex) === 3) { + list($red, $green, $blue) = [ + $hex[0] . $hex[0], + $hex[1] . $hex[1], + $hex[2] . $hex[2] + ]; + } elseif(strlen($hex) === 6) { + list($red, $green, $blue) = [ + $hex[0] . $hex[1], + $hex[2] . $hex[3], + $hex[4] . $hex[5] + ]; + } else { + throw new \Exception("Invalid color value: $color", self::ERR_INVALID_COLOR); + } + + // Turn color into an array + $color = [ + 'red' => hexdec($red), + 'green' => hexdec($green), + 'blue' => hexdec($blue), + 'alpha' => $alpha + ]; + } + + // Enforce color value ranges + if(is_array($color)) { + // RGB default to 0 + $color['red'] = isset($color['red']) ? $color['red'] : 0; + $color['green'] = isset($color['green']) ? $color['green'] : 0; + $color['blue'] = isset($color['blue']) ? $color['blue'] : 0; + + // Alpha defaults to 1 + $color['alpha'] = isset($color['alpha']) ? $color['alpha'] : 1; + + return [ + 'red' => (int) self::keepWithin((int) $color['red'], 0, 255), + 'green' => (int) self::keepWithin((int) $color['green'], 0, 255), + 'blue' => (int) self::keepWithin((int) $color['blue'], 0, 255), + 'alpha' => self::keepWithin($color['alpha'], 0, 1) + ]; + } + + throw new \Exception("Invalid color value: $color", self::ERR_INVALID_COLOR); + } + +} diff --git a/kirby/vendor/composer/ClassLoader.php b/kirby/vendor/composer/ClassLoader.php new file mode 100755 index 0000000..fce8549 --- /dev/null +++ b/kirby/vendor/composer/ClassLoader.php @@ -0,0 +1,445 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see http://www.php-fig.org/psr/psr-0/ + * @see http://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + private $classMapAuthoritative = false; + private $missingClasses = array(); + private $apcuPrefix; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/kirby/vendor/composer/autoload_classmap.php b/kirby/vendor/composer/autoload_classmap.php new file mode 100755 index 0000000..e2bb961 --- /dev/null +++ b/kirby/vendor/composer/autoload_classmap.php @@ -0,0 +1,267 @@ + $baseDir . '/src/Api/Api.php', + 'Kirby\\Api\\Collection' => $baseDir . '/src/Api/Collection.php', + 'Kirby\\Api\\Model' => $baseDir . '/src/Api/Model.php', + 'Kirby\\Cache\\ApcuCache' => $baseDir . '/src/Cache/ApcuCache.php', + 'Kirby\\Cache\\Cache' => $baseDir . '/src/Cache/Cache.php', + 'Kirby\\Cache\\FileCache' => $baseDir . '/src/Cache/FileCache.php', + 'Kirby\\Cache\\MemCached' => $baseDir . '/src/Cache/MemCached.php', + 'Kirby\\Cache\\MemoryCache' => $baseDir . '/src/Cache/MemoryCache.php', + 'Kirby\\Cache\\NullCache' => $baseDir . '/src/Cache/NullCache.php', + 'Kirby\\Cache\\Value' => $baseDir . '/src/Cache/Value.php', + 'Kirby\\Cms\\Api' => $baseDir . '/src/Cms/Api.php', + 'Kirby\\Cms\\App' => $baseDir . '/src/Cms/App.php', + 'Kirby\\Cms\\AppCaches' => $baseDir . '/src/Cms/AppCaches.php', + 'Kirby\\Cms\\AppErrors' => $baseDir . '/src/Cms/AppErrors.php', + 'Kirby\\Cms\\AppPlugins' => $baseDir . '/src/Cms/AppPlugins.php', + 'Kirby\\Cms\\AppTranslations' => $baseDir . '/src/Cms/AppTranslations.php', + 'Kirby\\Cms\\AppUsers' => $baseDir . '/src/Cms/AppUsers.php', + 'Kirby\\Cms\\Asset' => $baseDir . '/src/Cms/Asset.php', + 'Kirby\\Cms\\Auth' => $baseDir . '/src/Cms/Auth.php', + 'Kirby\\Cms\\Blueprint' => $baseDir . '/src/Cms/Blueprint.php', + 'Kirby\\Cms\\Collection' => $baseDir . '/src/Cms/Collection.php', + 'Kirby\\Cms\\Collections' => $baseDir . '/src/Cms/Collections.php', + 'Kirby\\Cms\\Content' => $baseDir . '/src/Cms/Content.php', + 'Kirby\\Cms\\ContentLock' => $baseDir . '/src/Cms/ContentLock.php', + 'Kirby\\Cms\\ContentLocks' => $baseDir . '/src/Cms/ContentLocks.php', + 'Kirby\\Cms\\ContentTranslation' => $baseDir . '/src/Cms/ContentTranslation.php', + 'Kirby\\Cms\\Dir' => $baseDir . '/src/Cms/Dir.php', + 'Kirby\\Cms\\Email' => $baseDir . '/src/Cms/Email.php', + 'Kirby\\Cms\\Field' => $baseDir . '/src/Cms/Field.php', + 'Kirby\\Cms\\File' => $baseDir . '/src/Cms/File.php', + 'Kirby\\Cms\\FileActions' => $baseDir . '/src/Cms/FileActions.php', + 'Kirby\\Cms\\FileBlueprint' => $baseDir . '/src/Cms/FileBlueprint.php', + 'Kirby\\Cms\\FileFoundation' => $baseDir . '/src/Cms/FileFoundation.php', + 'Kirby\\Cms\\FileModifications' => $baseDir . '/src/Cms/FileModifications.php', + 'Kirby\\Cms\\FilePermissions' => $baseDir . '/src/Cms/FilePermissions.php', + 'Kirby\\Cms\\FilePicker' => $baseDir . '/src/Cms/FilePicker.php', + 'Kirby\\Cms\\FileRules' => $baseDir . '/src/Cms/FileRules.php', + 'Kirby\\Cms\\FileVersion' => $baseDir . '/src/Cms/FileVersion.php', + 'Kirby\\Cms\\Filename' => $baseDir . '/src/Cms/Filename.php', + 'Kirby\\Cms\\Files' => $baseDir . '/src/Cms/Files.php', + 'Kirby\\Cms\\Form' => $baseDir . '/src/Cms/Form.php', + 'Kirby\\Cms\\HasChildren' => $baseDir . '/src/Cms/HasChildren.php', + 'Kirby\\Cms\\HasFiles' => $baseDir . '/src/Cms/HasFiles.php', + 'Kirby\\Cms\\HasMethods' => $baseDir . '/src/Cms/HasMethods.php', + 'Kirby\\Cms\\HasSiblings' => $baseDir . '/src/Cms/HasSiblings.php', + 'Kirby\\Cms\\Html' => $baseDir . '/src/Cms/Html.php', + 'Kirby\\Cms\\Ingredients' => $baseDir . '/src/Cms/Ingredients.php', + 'Kirby\\Cms\\KirbyTag' => $baseDir . '/src/Cms/KirbyTag.php', + 'Kirby\\Cms\\KirbyTags' => $baseDir . '/src/Cms/KirbyTags.php', + 'Kirby\\Cms\\Language' => $baseDir . '/src/Cms/Language.php', + 'Kirby\\Cms\\LanguageRouter' => $baseDir . '/src/Cms/LanguageRouter.php', + 'Kirby\\Cms\\LanguageRoutes' => $baseDir . '/src/Cms/LanguageRoutes.php', + 'Kirby\\Cms\\LanguageRules' => $baseDir . '/src/Cms/LanguageRules.php', + 'Kirby\\Cms\\Languages' => $baseDir . '/src/Cms/Languages.php', + 'Kirby\\Cms\\Media' => $baseDir . '/src/Cms/Media.php', + 'Kirby\\Cms\\Model' => $baseDir . '/src/Cms/Model.php', + 'Kirby\\Cms\\ModelPermissions' => $baseDir . '/src/Cms/ModelPermissions.php', + 'Kirby\\Cms\\ModelWithContent' => $baseDir . '/src/Cms/ModelWithContent.php', + 'Kirby\\Cms\\Nest' => $baseDir . '/src/Cms/Nest.php', + 'Kirby\\Cms\\NestCollection' => $baseDir . '/src/Cms/NestCollection.php', + 'Kirby\\Cms\\NestObject' => $baseDir . '/src/Cms/NestObject.php', + 'Kirby\\Cms\\Page' => $baseDir . '/src/Cms/Page.php', + 'Kirby\\Cms\\PageActions' => $baseDir . '/src/Cms/PageActions.php', + 'Kirby\\Cms\\PageBlueprint' => $baseDir . '/src/Cms/PageBlueprint.php', + 'Kirby\\Cms\\PagePermissions' => $baseDir . '/src/Cms/PagePermissions.php', + 'Kirby\\Cms\\PagePicker' => $baseDir . '/src/Cms/PagePicker.php', + 'Kirby\\Cms\\PageRules' => $baseDir . '/src/Cms/PageRules.php', + 'Kirby\\Cms\\PageSiblings' => $baseDir . '/src/Cms/PageSiblings.php', + 'Kirby\\Cms\\Pages' => $baseDir . '/src/Cms/Pages.php', + 'Kirby\\Cms\\Pagination' => $baseDir . '/src/Cms/Pagination.php', + 'Kirby\\Cms\\Panel' => $baseDir . '/src/Cms/Panel.php', + 'Kirby\\Cms\\PanelPlugins' => $baseDir . '/src/Cms/PanelPlugins.php', + 'Kirby\\Cms\\Permissions' => $baseDir . '/src/Cms/Permissions.php', + 'Kirby\\Cms\\Picker' => $baseDir . '/src/Cms/Picker.php', + 'Kirby\\Cms\\Plugin' => $baseDir . '/src/Cms/Plugin.php', + 'Kirby\\Cms\\PluginAssets' => $baseDir . '/src/Cms/PluginAssets.php', + 'Kirby\\Cms\\R' => $baseDir . '/src/Cms/R.php', + 'Kirby\\Cms\\Responder' => $baseDir . '/src/Cms/Responder.php', + 'Kirby\\Cms\\Response' => $baseDir . '/src/Cms/Response.php', + 'Kirby\\Cms\\Role' => $baseDir . '/src/Cms/Role.php', + 'Kirby\\Cms\\Roles' => $baseDir . '/src/Cms/Roles.php', + 'Kirby\\Cms\\S' => $baseDir . '/src/Cms/S.php', + 'Kirby\\Cms\\Search' => $baseDir . '/src/Cms/Search.php', + 'Kirby\\Cms\\Section' => $baseDir . '/src/Cms/Section.php', + 'Kirby\\Cms\\Site' => $baseDir . '/src/Cms/Site.php', + 'Kirby\\Cms\\SiteActions' => $baseDir . '/src/Cms/SiteActions.php', + 'Kirby\\Cms\\SiteBlueprint' => $baseDir . '/src/Cms/SiteBlueprint.php', + 'Kirby\\Cms\\SitePermissions' => $baseDir . '/src/Cms/SitePermissions.php', + 'Kirby\\Cms\\SiteRules' => $baseDir . '/src/Cms/SiteRules.php', + 'Kirby\\Cms\\Structure' => $baseDir . '/src/Cms/Structure.php', + 'Kirby\\Cms\\StructureObject' => $baseDir . '/src/Cms/StructureObject.php', + 'Kirby\\Cms\\System' => $baseDir . '/src/Cms/System.php', + 'Kirby\\Cms\\Template' => $baseDir . '/src/Cms/Template.php', + 'Kirby\\Cms\\Translation' => $baseDir . '/src/Cms/Translation.php', + 'Kirby\\Cms\\Translations' => $baseDir . '/src/Cms/Translations.php', + 'Kirby\\Cms\\Url' => $baseDir . '/src/Cms/Url.php', + 'Kirby\\Cms\\User' => $baseDir . '/src/Cms/User.php', + 'Kirby\\Cms\\UserActions' => $baseDir . '/src/Cms/UserActions.php', + 'Kirby\\Cms\\UserBlueprint' => $baseDir . '/src/Cms/UserBlueprint.php', + 'Kirby\\Cms\\UserPermissions' => $baseDir . '/src/Cms/UserPermissions.php', + 'Kirby\\Cms\\UserPicker' => $baseDir . '/src/Cms/UserPicker.php', + 'Kirby\\Cms\\UserRules' => $baseDir . '/src/Cms/UserRules.php', + 'Kirby\\Cms\\Users' => $baseDir . '/src/Cms/Users.php', + 'Kirby\\Cms\\Visitor' => $baseDir . '/src/Cms/Visitor.php', + 'Kirby\\ComposerInstaller\\CmsInstaller' => $vendorDir . '/getkirby/composer-installer/src/ComposerInstaller/CmsInstaller.php', + 'Kirby\\ComposerInstaller\\Installer' => $vendorDir . '/getkirby/composer-installer/src/ComposerInstaller/Installer.php', + 'Kirby\\ComposerInstaller\\Plugin' => $vendorDir . '/getkirby/composer-installer/src/ComposerInstaller/Plugin.php', + 'Kirby\\ComposerInstaller\\PluginInstaller' => $vendorDir . '/getkirby/composer-installer/src/ComposerInstaller/PluginInstaller.php', + 'Kirby\\Data\\Data' => $baseDir . '/src/Data/Data.php', + 'Kirby\\Data\\Handler' => $baseDir . '/src/Data/Handler.php', + 'Kirby\\Data\\Json' => $baseDir . '/src/Data/Json.php', + 'Kirby\\Data\\PHP' => $baseDir . '/src/Data/PHP.php', + 'Kirby\\Data\\Txt' => $baseDir . '/src/Data/Txt.php', + 'Kirby\\Data\\Yaml' => $baseDir . '/src/Data/Yaml.php', + 'Kirby\\Database\\Database' => $baseDir . '/src/Database/Database.php', + 'Kirby\\Database\\Db' => $baseDir . '/src/Database/Db.php', + 'Kirby\\Database\\Query' => $baseDir . '/src/Database/Query.php', + 'Kirby\\Database\\Sql' => $baseDir . '/src/Database/Sql.php', + 'Kirby\\Database\\Sql\\Mysql' => $baseDir . '/src/Database/Sql/Mysql.php', + 'Kirby\\Database\\Sql\\Sqlite' => $baseDir . '/src/Database/Sql/Sqlite.php', + 'Kirby\\Email\\Body' => $baseDir . '/src/Email/Body.php', + 'Kirby\\Email\\Email' => $baseDir . '/src/Email/Email.php', + 'Kirby\\Email\\PHPMailer' => $baseDir . '/src/Email/PHPMailer.php', + 'Kirby\\Exception\\BadMethodCallException' => $baseDir . '/src/Exception/BadMethodCallException.php', + 'Kirby\\Exception\\DuplicateException' => $baseDir . '/src/Exception/DuplicateException.php', + 'Kirby\\Exception\\ErrorPageException' => $baseDir . '/src/Exception/ErrorPageException.php', + 'Kirby\\Exception\\Exception' => $baseDir . '/src/Exception/Exception.php', + 'Kirby\\Exception\\InvalidArgumentException' => $baseDir . '/src/Exception/InvalidArgumentException.php', + 'Kirby\\Exception\\LogicException' => $baseDir . '/src/Exception/LogicException.php', + 'Kirby\\Exception\\NotFoundException' => $baseDir . '/src/Exception/NotFoundException.php', + 'Kirby\\Exception\\PermissionException' => $baseDir . '/src/Exception/PermissionException.php', + 'Kirby\\Form\\Field' => $baseDir . '/src/Form/Field.php', + 'Kirby\\Form\\Fields' => $baseDir . '/src/Form/Fields.php', + 'Kirby\\Form\\Form' => $baseDir . '/src/Form/Form.php', + 'Kirby\\Form\\Options' => $baseDir . '/src/Form/Options.php', + 'Kirby\\Form\\OptionsApi' => $baseDir . '/src/Form/OptionsApi.php', + 'Kirby\\Form\\OptionsQuery' => $baseDir . '/src/Form/OptionsQuery.php', + 'Kirby\\Form\\Validations' => $baseDir . '/src/Form/Validations.php', + 'Kirby\\Http\\Cookie' => $baseDir . '/src/Http/Cookie.php', + 'Kirby\\Http\\Exceptions\\NextRouteException' => $baseDir . '/src/Http/Exceptions/NextRouteException.php', + 'Kirby\\Http\\Header' => $baseDir . '/src/Http/Header.php', + 'Kirby\\Http\\Idn' => $baseDir . '/src/Http/Idn.php', + 'Kirby\\Http\\Params' => $baseDir . '/src/Http/Params.php', + 'Kirby\\Http\\Path' => $baseDir . '/src/Http/Path.php', + 'Kirby\\Http\\Query' => $baseDir . '/src/Http/Query.php', + 'Kirby\\Http\\Remote' => $baseDir . '/src/Http/Remote.php', + 'Kirby\\Http\\Request' => $baseDir . '/src/Http/Request.php', + 'Kirby\\Http\\Request\\Auth\\BasicAuth' => $baseDir . '/src/Http/Request/Auth/BasicAuth.php', + 'Kirby\\Http\\Request\\Auth\\BearerAuth' => $baseDir . '/src/Http/Request/Auth/BearerAuth.php', + 'Kirby\\Http\\Request\\Body' => $baseDir . '/src/Http/Request/Body.php', + 'Kirby\\Http\\Request\\Data' => $baseDir . '/src/Http/Request/Data.php', + 'Kirby\\Http\\Request\\Files' => $baseDir . '/src/Http/Request/Files.php', + 'Kirby\\Http\\Request\\Query' => $baseDir . '/src/Http/Request/Query.php', + 'Kirby\\Http\\Response' => $baseDir . '/src/Http/Response.php', + 'Kirby\\Http\\Route' => $baseDir . '/src/Http/Route.php', + 'Kirby\\Http\\Router' => $baseDir . '/src/Http/Router.php', + 'Kirby\\Http\\Server' => $baseDir . '/src/Http/Server.php', + 'Kirby\\Http\\Uri' => $baseDir . '/src/Http/Uri.php', + 'Kirby\\Http\\Url' => $baseDir . '/src/Http/Url.php', + 'Kirby\\Http\\Visitor' => $baseDir . '/src/Http/Visitor.php', + 'Kirby\\Image\\Camera' => $baseDir . '/src/Image/Camera.php', + 'Kirby\\Image\\Darkroom' => $baseDir . '/src/Image/Darkroom.php', + 'Kirby\\Image\\Darkroom\\GdLib' => $baseDir . '/src/Image/Darkroom/GdLib.php', + 'Kirby\\Image\\Darkroom\\ImageMagick' => $baseDir . '/src/Image/Darkroom/ImageMagick.php', + 'Kirby\\Image\\Dimensions' => $baseDir . '/src/Image/Dimensions.php', + 'Kirby\\Image\\Exif' => $baseDir . '/src/Image/Exif.php', + 'Kirby\\Image\\Image' => $baseDir . '/src/Image/Image.php', + 'Kirby\\Image\\Location' => $baseDir . '/src/Image/Location.php', + 'Kirby\\Session\\AutoSession' => $baseDir . '/src/Session/AutoSession.php', + 'Kirby\\Session\\FileSessionStore' => $baseDir . '/src/Session/FileSessionStore.php', + 'Kirby\\Session\\Session' => $baseDir . '/src/Session/Session.php', + 'Kirby\\Session\\SessionData' => $baseDir . '/src/Session/SessionData.php', + 'Kirby\\Session\\SessionStore' => $baseDir . '/src/Session/SessionStore.php', + 'Kirby\\Session\\Sessions' => $baseDir . '/src/Session/Sessions.php', + 'Kirby\\Text\\KirbyTag' => $baseDir . '/src/Text/KirbyTag.php', + 'Kirby\\Text\\KirbyTags' => $baseDir . '/src/Text/KirbyTags.php', + 'Kirby\\Text\\Markdown' => $baseDir . '/src/Text/Markdown.php', + 'Kirby\\Text\\SmartyPants' => $baseDir . '/src/Text/SmartyPants.php', + 'Kirby\\Toolkit\\A' => $baseDir . '/src/Toolkit/A.php', + 'Kirby\\Toolkit\\Collection' => $baseDir . '/src/Toolkit/Collection.php', + 'Kirby\\Toolkit\\Component' => $baseDir . '/src/Toolkit/Component.php', + 'Kirby\\Toolkit\\Config' => $baseDir . '/src/Toolkit/Config.php', + 'Kirby\\Toolkit\\Controller' => $baseDir . '/src/Toolkit/Controller.php', + 'Kirby\\Toolkit\\Dir' => $baseDir . '/src/Toolkit/Dir.php', + 'Kirby\\Toolkit\\Escape' => $baseDir . '/src/Toolkit/Escape.php', + 'Kirby\\Toolkit\\F' => $baseDir . '/src/Toolkit/F.php', + 'Kirby\\Toolkit\\Facade' => $baseDir . '/src/Toolkit/Facade.php', + 'Kirby\\Toolkit\\File' => $baseDir . '/src/Toolkit/File.php', + 'Kirby\\Toolkit\\Html' => $baseDir . '/src/Toolkit/Html.php', + 'Kirby\\Toolkit\\I18n' => $baseDir . '/src/Toolkit/I18n.php', + 'Kirby\\Toolkit\\Iterator' => $baseDir . '/src/Toolkit/Iterator.php', + 'Kirby\\Toolkit\\Mime' => $baseDir . '/src/Toolkit/Mime.php', + 'Kirby\\Toolkit\\Obj' => $baseDir . '/src/Toolkit/Obj.php', + 'Kirby\\Toolkit\\Pagination' => $baseDir . '/src/Toolkit/Pagination.php', + 'Kirby\\Toolkit\\Properties' => $baseDir . '/src/Toolkit/Properties.php', + 'Kirby\\Toolkit\\Query' => $baseDir . '/src/Toolkit/Query.php', + 'Kirby\\Toolkit\\Silo' => $baseDir . '/src/Toolkit/Silo.php', + 'Kirby\\Toolkit\\Str' => $baseDir . '/src/Toolkit/Str.php', + 'Kirby\\Toolkit\\Tpl' => $baseDir . '/src/Toolkit/Tpl.php', + 'Kirby\\Toolkit\\V' => $baseDir . '/src/Toolkit/V.php', + 'Kirby\\Toolkit\\View' => $baseDir . '/src/Toolkit/View.php', + 'Kirby\\Toolkit\\Xml' => $baseDir . '/src/Toolkit/Xml.php', + 'Laminas\\Escaper\\Escaper' => $vendorDir . '/laminas/laminas-escaper/src/Escaper.php', + 'Laminas\\Escaper\\Exception\\ExceptionInterface' => $vendorDir . '/laminas/laminas-escaper/src/Exception/ExceptionInterface.php', + 'Laminas\\Escaper\\Exception\\InvalidArgumentException' => $vendorDir . '/laminas/laminas-escaper/src/Exception/InvalidArgumentException.php', + 'Laminas\\Escaper\\Exception\\RuntimeException' => $vendorDir . '/laminas/laminas-escaper/src/Exception/RuntimeException.php', + 'Laminas\\ZendFrameworkBridge\\Autoloader' => $vendorDir . '/laminas/laminas-zendframework-bridge/src/Autoloader.php', + 'Laminas\\ZendFrameworkBridge\\ConfigPostProcessor' => $vendorDir . '/laminas/laminas-zendframework-bridge/src/ConfigPostProcessor.php', + 'Laminas\\ZendFrameworkBridge\\Module' => $vendorDir . '/laminas/laminas-zendframework-bridge/src/Module.php', + 'Laminas\\ZendFrameworkBridge\\Replacements' => $vendorDir . '/laminas/laminas-zendframework-bridge/src/Replacements.php', + 'Laminas\\ZendFrameworkBridge\\RewriteRules' => $vendorDir . '/laminas/laminas-zendframework-bridge/src/RewriteRules.php', + 'League\\ColorExtractor\\Color' => $vendorDir . '/league/color-extractor/src/League/ColorExtractor/Color.php', + 'League\\ColorExtractor\\ColorExtractor' => $vendorDir . '/league/color-extractor/src/League/ColorExtractor/ColorExtractor.php', + 'League\\ColorExtractor\\Palette' => $vendorDir . '/league/color-extractor/src/League/ColorExtractor/Palette.php', + 'Michelf\\SmartyPants' => $vendorDir . '/michelf/php-smartypants/Michelf/SmartyPants.php', + 'Michelf\\SmartyPantsTypographer' => $vendorDir . '/michelf/php-smartypants/Michelf/SmartyPantsTypographer.php', + 'PHPMailer\\PHPMailer\\Exception' => $vendorDir . '/phpmailer/phpmailer/src/Exception.php', + 'PHPMailer\\PHPMailer\\OAuth' => $vendorDir . '/phpmailer/phpmailer/src/OAuth.php', + 'PHPMailer\\PHPMailer\\PHPMailer' => $vendorDir . '/phpmailer/phpmailer/src/PHPMailer.php', + 'PHPMailer\\PHPMailer\\POP3' => $vendorDir . '/phpmailer/phpmailer/src/POP3.php', + 'PHPMailer\\PHPMailer\\SMTP' => $vendorDir . '/phpmailer/phpmailer/src/SMTP.php', + 'Parsedown' => $baseDir . '/dependencies/parsedown/Parsedown.php', + 'ParsedownExtra' => $baseDir . '/dependencies/parsedown-extra/ParsedownExtra.php', + 'Psr\\Log\\AbstractLogger' => $vendorDir . '/psr/log/Psr/Log/AbstractLogger.php', + 'Psr\\Log\\InvalidArgumentException' => $vendorDir . '/psr/log/Psr/Log/InvalidArgumentException.php', + 'Psr\\Log\\LogLevel' => $vendorDir . '/psr/log/Psr/Log/LogLevel.php', + 'Psr\\Log\\LoggerAwareInterface' => $vendorDir . '/psr/log/Psr/Log/LoggerAwareInterface.php', + 'Psr\\Log\\LoggerAwareTrait' => $vendorDir . '/psr/log/Psr/Log/LoggerAwareTrait.php', + 'Psr\\Log\\LoggerInterface' => $vendorDir . '/psr/log/Psr/Log/LoggerInterface.php', + 'Psr\\Log\\LoggerTrait' => $vendorDir . '/psr/log/Psr/Log/LoggerTrait.php', + 'Psr\\Log\\NullLogger' => $vendorDir . '/psr/log/Psr/Log/NullLogger.php', + 'Psr\\Log\\Test\\DummyTest' => $vendorDir . '/psr/log/Psr/Log/Test/LoggerInterfaceTest.php', + 'Psr\\Log\\Test\\LoggerInterfaceTest' => $vendorDir . '/psr/log/Psr/Log/Test/LoggerInterfaceTest.php', + 'Psr\\Log\\Test\\TestLogger' => $vendorDir . '/psr/log/Psr/Log/Test/TestLogger.php', + 'Symfony\\Polyfill\\Mbstring\\Mbstring' => $vendorDir . '/symfony/polyfill-mbstring/Mbstring.php', + 'TrueBV\\Exception\\DomainOutOfBoundsException' => $vendorDir . '/true/punycode/src/Exception/DomainOutOfBoundsException.php', + 'TrueBV\\Exception\\LabelOutOfBoundsException' => $vendorDir . '/true/punycode/src/Exception/LabelOutOfBoundsException.php', + 'TrueBV\\Exception\\OutOfBoundsException' => $vendorDir . '/true/punycode/src/Exception/OutOfBoundsException.php', + 'TrueBV\\Punycode' => $vendorDir . '/true/punycode/src/Punycode.php', + 'Whoops\\Exception\\ErrorException' => $vendorDir . '/filp/whoops/src/Whoops/Exception/ErrorException.php', + 'Whoops\\Exception\\Formatter' => $vendorDir . '/filp/whoops/src/Whoops/Exception/Formatter.php', + 'Whoops\\Exception\\Frame' => $vendorDir . '/filp/whoops/src/Whoops/Exception/Frame.php', + 'Whoops\\Exception\\FrameCollection' => $vendorDir . '/filp/whoops/src/Whoops/Exception/FrameCollection.php', + 'Whoops\\Exception\\Inspector' => $vendorDir . '/filp/whoops/src/Whoops/Exception/Inspector.php', + 'Whoops\\Handler\\CallbackHandler' => $vendorDir . '/filp/whoops/src/Whoops/Handler/CallbackHandler.php', + 'Whoops\\Handler\\Handler' => $vendorDir . '/filp/whoops/src/Whoops/Handler/Handler.php', + 'Whoops\\Handler\\HandlerInterface' => $vendorDir . '/filp/whoops/src/Whoops/Handler/HandlerInterface.php', + 'Whoops\\Handler\\JsonResponseHandler' => $vendorDir . '/filp/whoops/src/Whoops/Handler/JsonResponseHandler.php', + 'Whoops\\Handler\\PlainTextHandler' => $vendorDir . '/filp/whoops/src/Whoops/Handler/PlainTextHandler.php', + 'Whoops\\Handler\\PrettyPageHandler' => $vendorDir . '/filp/whoops/src/Whoops/Handler/PrettyPageHandler.php', + 'Whoops\\Handler\\XmlResponseHandler' => $vendorDir . '/filp/whoops/src/Whoops/Handler/XmlResponseHandler.php', + 'Whoops\\Run' => $vendorDir . '/filp/whoops/src/Whoops/Run.php', + 'Whoops\\RunInterface' => $vendorDir . '/filp/whoops/src/Whoops/RunInterface.php', + 'Whoops\\Util\\HtmlDumperOutput' => $vendorDir . '/filp/whoops/src/Whoops/Util/HtmlDumperOutput.php', + 'Whoops\\Util\\Misc' => $vendorDir . '/filp/whoops/src/Whoops/Util/Misc.php', + 'Whoops\\Util\\SystemFacade' => $vendorDir . '/filp/whoops/src/Whoops/Util/SystemFacade.php', + 'Whoops\\Util\\TemplateHelper' => $vendorDir . '/filp/whoops/src/Whoops/Util/TemplateHelper.php', + 'claviska\\SimpleImage' => $vendorDir . '/claviska/simpleimage/src/claviska/SimpleImage.php', +); diff --git a/kirby/vendor/composer/autoload_files.php b/kirby/vendor/composer/autoload_files.php new file mode 100755 index 0000000..7d22d07 --- /dev/null +++ b/kirby/vendor/composer/autoload_files.php @@ -0,0 +1,13 @@ + $vendorDir . '/laminas/laminas-zendframework-bridge/src/autoload.php', + '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', + '04c6c5c2f7095ccf6c481d3e53e1776f' => $vendorDir . '/mustangostang/spyc/Spyc.php', + 'f864ae44e8154e5ff6f4eec32f46d37f' => $baseDir . '/config/setup.php', +); diff --git a/kirby/vendor/composer/autoload_namespaces.php b/kirby/vendor/composer/autoload_namespaces.php new file mode 100755 index 0000000..f135c18 --- /dev/null +++ b/kirby/vendor/composer/autoload_namespaces.php @@ -0,0 +1,11 @@ + array($vendorDir . '/claviska/simpleimage/src'), + 'Michelf' => array($vendorDir . '/michelf/php-smartypants'), +); diff --git a/kirby/vendor/composer/autoload_psr4.php b/kirby/vendor/composer/autoload_psr4.php new file mode 100755 index 0000000..1fc37de --- /dev/null +++ b/kirby/vendor/composer/autoload_psr4.php @@ -0,0 +1,18 @@ + array($vendorDir . '/filp/whoops/src/Whoops'), + 'TrueBV\\' => array($vendorDir . '/true/punycode/src'), + 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), + 'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'), + 'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'), + 'Laminas\\ZendFrameworkBridge\\' => array($vendorDir . '/laminas/laminas-zendframework-bridge/src'), + 'Laminas\\Escaper\\' => array($vendorDir . '/laminas/laminas-escaper/src'), + 'Kirby\\' => array($baseDir . '/src', $vendorDir . '/getkirby/composer-installer/src'), + '' => array($vendorDir . '/league/color-extractor/src'), +); diff --git a/kirby/vendor/composer/autoload_real.php b/kirby/vendor/composer/autoload_real.php new file mode 100755 index 0000000..b85634b --- /dev/null +++ b/kirby/vendor/composer/autoload_real.php @@ -0,0 +1,70 @@ += 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require_once __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInit12091bebabd81c9aba88b2aeec22c8d7::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + if ($useStaticLoader) { + $includeFiles = Composer\Autoload\ComposerStaticInit12091bebabd81c9aba88b2aeec22c8d7::$files; + } else { + $includeFiles = require __DIR__ . '/autoload_files.php'; + } + foreach ($includeFiles as $fileIdentifier => $file) { + composerRequire12091bebabd81c9aba88b2aeec22c8d7($fileIdentifier, $file); + } + + return $loader; + } +} + +function composerRequire12091bebabd81c9aba88b2aeec22c8d7($fileIdentifier, $file) +{ + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + require $file; + + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + } +} diff --git a/kirby/vendor/composer/autoload_static.php b/kirby/vendor/composer/autoload_static.php new file mode 100755 index 0000000..e568d0d --- /dev/null +++ b/kirby/vendor/composer/autoload_static.php @@ -0,0 +1,374 @@ + __DIR__ . '/..' . '/laminas/laminas-zendframework-bridge/src/autoload.php', + '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', + '04c6c5c2f7095ccf6c481d3e53e1776f' => __DIR__ . '/..' . '/mustangostang/spyc/Spyc.php', + 'f864ae44e8154e5ff6f4eec32f46d37f' => __DIR__ . '/../..' . '/config/setup.php', + ); + + public static $prefixLengthsPsr4 = array ( + 'W' => + array ( + 'Whoops\\' => 7, + ), + 'T' => + array ( + 'TrueBV\\' => 7, + ), + 'S' => + array ( + 'Symfony\\Polyfill\\Mbstring\\' => 26, + ), + 'P' => + array ( + 'Psr\\Log\\' => 8, + 'PHPMailer\\PHPMailer\\' => 20, + ), + 'L' => + array ( + 'Laminas\\ZendFrameworkBridge\\' => 28, + 'Laminas\\Escaper\\' => 16, + ), + 'K' => + array ( + 'Kirby\\' => 6, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'Whoops\\' => + array ( + 0 => __DIR__ . '/..' . '/filp/whoops/src/Whoops', + ), + 'TrueBV\\' => + array ( + 0 => __DIR__ . '/..' . '/true/punycode/src', + ), + 'Symfony\\Polyfill\\Mbstring\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring', + ), + 'Psr\\Log\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/log/Psr/Log', + ), + 'PHPMailer\\PHPMailer\\' => + array ( + 0 => __DIR__ . '/..' . '/phpmailer/phpmailer/src', + ), + 'Laminas\\ZendFrameworkBridge\\' => + array ( + 0 => __DIR__ . '/..' . '/laminas/laminas-zendframework-bridge/src', + ), + 'Laminas\\Escaper\\' => + array ( + 0 => __DIR__ . '/..' . '/laminas/laminas-escaper/src', + ), + 'Kirby\\' => + array ( + 0 => __DIR__ . '/../..' . '/src', + 1 => __DIR__ . '/..' . '/getkirby/composer-installer/src', + ), + ); + + public static $fallbackDirsPsr4 = array ( + 0 => __DIR__ . '/..' . '/league/color-extractor/src', + ); + + public static $prefixesPsr0 = array ( + 'c' => + array ( + 'claviska' => + array ( + 0 => __DIR__ . '/..' . '/claviska/simpleimage/src', + ), + ), + 'M' => + array ( + 'Michelf' => + array ( + 0 => __DIR__ . '/..' . '/michelf/php-smartypants', + ), + ), + ); + + public static $classMap = array ( + 'Kirby\\Api\\Api' => __DIR__ . '/../..' . '/src/Api/Api.php', + 'Kirby\\Api\\Collection' => __DIR__ . '/../..' . '/src/Api/Collection.php', + 'Kirby\\Api\\Model' => __DIR__ . '/../..' . '/src/Api/Model.php', + 'Kirby\\Cache\\ApcuCache' => __DIR__ . '/../..' . '/src/Cache/ApcuCache.php', + 'Kirby\\Cache\\Cache' => __DIR__ . '/../..' . '/src/Cache/Cache.php', + 'Kirby\\Cache\\FileCache' => __DIR__ . '/../..' . '/src/Cache/FileCache.php', + 'Kirby\\Cache\\MemCached' => __DIR__ . '/../..' . '/src/Cache/MemCached.php', + 'Kirby\\Cache\\MemoryCache' => __DIR__ . '/../..' . '/src/Cache/MemoryCache.php', + 'Kirby\\Cache\\NullCache' => __DIR__ . '/../..' . '/src/Cache/NullCache.php', + 'Kirby\\Cache\\Value' => __DIR__ . '/../..' . '/src/Cache/Value.php', + 'Kirby\\Cms\\Api' => __DIR__ . '/../..' . '/src/Cms/Api.php', + 'Kirby\\Cms\\App' => __DIR__ . '/../..' . '/src/Cms/App.php', + 'Kirby\\Cms\\AppCaches' => __DIR__ . '/../..' . '/src/Cms/AppCaches.php', + 'Kirby\\Cms\\AppErrors' => __DIR__ . '/../..' . '/src/Cms/AppErrors.php', + 'Kirby\\Cms\\AppPlugins' => __DIR__ . '/../..' . '/src/Cms/AppPlugins.php', + 'Kirby\\Cms\\AppTranslations' => __DIR__ . '/../..' . '/src/Cms/AppTranslations.php', + 'Kirby\\Cms\\AppUsers' => __DIR__ . '/../..' . '/src/Cms/AppUsers.php', + 'Kirby\\Cms\\Asset' => __DIR__ . '/../..' . '/src/Cms/Asset.php', + 'Kirby\\Cms\\Auth' => __DIR__ . '/../..' . '/src/Cms/Auth.php', + 'Kirby\\Cms\\Blueprint' => __DIR__ . '/../..' . '/src/Cms/Blueprint.php', + 'Kirby\\Cms\\Collection' => __DIR__ . '/../..' . '/src/Cms/Collection.php', + 'Kirby\\Cms\\Collections' => __DIR__ . '/../..' . '/src/Cms/Collections.php', + 'Kirby\\Cms\\Content' => __DIR__ . '/../..' . '/src/Cms/Content.php', + 'Kirby\\Cms\\ContentLock' => __DIR__ . '/../..' . '/src/Cms/ContentLock.php', + 'Kirby\\Cms\\ContentLocks' => __DIR__ . '/../..' . '/src/Cms/ContentLocks.php', + 'Kirby\\Cms\\ContentTranslation' => __DIR__ . '/../..' . '/src/Cms/ContentTranslation.php', + 'Kirby\\Cms\\Dir' => __DIR__ . '/../..' . '/src/Cms/Dir.php', + 'Kirby\\Cms\\Email' => __DIR__ . '/../..' . '/src/Cms/Email.php', + 'Kirby\\Cms\\Field' => __DIR__ . '/../..' . '/src/Cms/Field.php', + 'Kirby\\Cms\\File' => __DIR__ . '/../..' . '/src/Cms/File.php', + 'Kirby\\Cms\\FileActions' => __DIR__ . '/../..' . '/src/Cms/FileActions.php', + 'Kirby\\Cms\\FileBlueprint' => __DIR__ . '/../..' . '/src/Cms/FileBlueprint.php', + 'Kirby\\Cms\\FileFoundation' => __DIR__ . '/../..' . '/src/Cms/FileFoundation.php', + 'Kirby\\Cms\\FileModifications' => __DIR__ . '/../..' . '/src/Cms/FileModifications.php', + 'Kirby\\Cms\\FilePermissions' => __DIR__ . '/../..' . '/src/Cms/FilePermissions.php', + 'Kirby\\Cms\\FilePicker' => __DIR__ . '/../..' . '/src/Cms/FilePicker.php', + 'Kirby\\Cms\\FileRules' => __DIR__ . '/../..' . '/src/Cms/FileRules.php', + 'Kirby\\Cms\\FileVersion' => __DIR__ . '/../..' . '/src/Cms/FileVersion.php', + 'Kirby\\Cms\\Filename' => __DIR__ . '/../..' . '/src/Cms/Filename.php', + 'Kirby\\Cms\\Files' => __DIR__ . '/../..' . '/src/Cms/Files.php', + 'Kirby\\Cms\\Form' => __DIR__ . '/../..' . '/src/Cms/Form.php', + 'Kirby\\Cms\\HasChildren' => __DIR__ . '/../..' . '/src/Cms/HasChildren.php', + 'Kirby\\Cms\\HasFiles' => __DIR__ . '/../..' . '/src/Cms/HasFiles.php', + 'Kirby\\Cms\\HasMethods' => __DIR__ . '/../..' . '/src/Cms/HasMethods.php', + 'Kirby\\Cms\\HasSiblings' => __DIR__ . '/../..' . '/src/Cms/HasSiblings.php', + 'Kirby\\Cms\\Html' => __DIR__ . '/../..' . '/src/Cms/Html.php', + 'Kirby\\Cms\\Ingredients' => __DIR__ . '/../..' . '/src/Cms/Ingredients.php', + 'Kirby\\Cms\\KirbyTag' => __DIR__ . '/../..' . '/src/Cms/KirbyTag.php', + 'Kirby\\Cms\\KirbyTags' => __DIR__ . '/../..' . '/src/Cms/KirbyTags.php', + 'Kirby\\Cms\\Language' => __DIR__ . '/../..' . '/src/Cms/Language.php', + 'Kirby\\Cms\\LanguageRouter' => __DIR__ . '/../..' . '/src/Cms/LanguageRouter.php', + 'Kirby\\Cms\\LanguageRoutes' => __DIR__ . '/../..' . '/src/Cms/LanguageRoutes.php', + 'Kirby\\Cms\\LanguageRules' => __DIR__ . '/../..' . '/src/Cms/LanguageRules.php', + 'Kirby\\Cms\\Languages' => __DIR__ . '/../..' . '/src/Cms/Languages.php', + 'Kirby\\Cms\\Media' => __DIR__ . '/../..' . '/src/Cms/Media.php', + 'Kirby\\Cms\\Model' => __DIR__ . '/../..' . '/src/Cms/Model.php', + 'Kirby\\Cms\\ModelPermissions' => __DIR__ . '/../..' . '/src/Cms/ModelPermissions.php', + 'Kirby\\Cms\\ModelWithContent' => __DIR__ . '/../..' . '/src/Cms/ModelWithContent.php', + 'Kirby\\Cms\\Nest' => __DIR__ . '/../..' . '/src/Cms/Nest.php', + 'Kirby\\Cms\\NestCollection' => __DIR__ . '/../..' . '/src/Cms/NestCollection.php', + 'Kirby\\Cms\\NestObject' => __DIR__ . '/../..' . '/src/Cms/NestObject.php', + 'Kirby\\Cms\\Page' => __DIR__ . '/../..' . '/src/Cms/Page.php', + 'Kirby\\Cms\\PageActions' => __DIR__ . '/../..' . '/src/Cms/PageActions.php', + 'Kirby\\Cms\\PageBlueprint' => __DIR__ . '/../..' . '/src/Cms/PageBlueprint.php', + 'Kirby\\Cms\\PagePermissions' => __DIR__ . '/../..' . '/src/Cms/PagePermissions.php', + 'Kirby\\Cms\\PagePicker' => __DIR__ . '/../..' . '/src/Cms/PagePicker.php', + 'Kirby\\Cms\\PageRules' => __DIR__ . '/../..' . '/src/Cms/PageRules.php', + 'Kirby\\Cms\\PageSiblings' => __DIR__ . '/../..' . '/src/Cms/PageSiblings.php', + 'Kirby\\Cms\\Pages' => __DIR__ . '/../..' . '/src/Cms/Pages.php', + 'Kirby\\Cms\\Pagination' => __DIR__ . '/../..' . '/src/Cms/Pagination.php', + 'Kirby\\Cms\\Panel' => __DIR__ . '/../..' . '/src/Cms/Panel.php', + 'Kirby\\Cms\\PanelPlugins' => __DIR__ . '/../..' . '/src/Cms/PanelPlugins.php', + 'Kirby\\Cms\\Permissions' => __DIR__ . '/../..' . '/src/Cms/Permissions.php', + 'Kirby\\Cms\\Picker' => __DIR__ . '/../..' . '/src/Cms/Picker.php', + 'Kirby\\Cms\\Plugin' => __DIR__ . '/../..' . '/src/Cms/Plugin.php', + 'Kirby\\Cms\\PluginAssets' => __DIR__ . '/../..' . '/src/Cms/PluginAssets.php', + 'Kirby\\Cms\\R' => __DIR__ . '/../..' . '/src/Cms/R.php', + 'Kirby\\Cms\\Responder' => __DIR__ . '/../..' . '/src/Cms/Responder.php', + 'Kirby\\Cms\\Response' => __DIR__ . '/../..' . '/src/Cms/Response.php', + 'Kirby\\Cms\\Role' => __DIR__ . '/../..' . '/src/Cms/Role.php', + 'Kirby\\Cms\\Roles' => __DIR__ . '/../..' . '/src/Cms/Roles.php', + 'Kirby\\Cms\\S' => __DIR__ . '/../..' . '/src/Cms/S.php', + 'Kirby\\Cms\\Search' => __DIR__ . '/../..' . '/src/Cms/Search.php', + 'Kirby\\Cms\\Section' => __DIR__ . '/../..' . '/src/Cms/Section.php', + 'Kirby\\Cms\\Site' => __DIR__ . '/../..' . '/src/Cms/Site.php', + 'Kirby\\Cms\\SiteActions' => __DIR__ . '/../..' . '/src/Cms/SiteActions.php', + 'Kirby\\Cms\\SiteBlueprint' => __DIR__ . '/../..' . '/src/Cms/SiteBlueprint.php', + 'Kirby\\Cms\\SitePermissions' => __DIR__ . '/../..' . '/src/Cms/SitePermissions.php', + 'Kirby\\Cms\\SiteRules' => __DIR__ . '/../..' . '/src/Cms/SiteRules.php', + 'Kirby\\Cms\\Structure' => __DIR__ . '/../..' . '/src/Cms/Structure.php', + 'Kirby\\Cms\\StructureObject' => __DIR__ . '/../..' . '/src/Cms/StructureObject.php', + 'Kirby\\Cms\\System' => __DIR__ . '/../..' . '/src/Cms/System.php', + 'Kirby\\Cms\\Template' => __DIR__ . '/../..' . '/src/Cms/Template.php', + 'Kirby\\Cms\\Translation' => __DIR__ . '/../..' . '/src/Cms/Translation.php', + 'Kirby\\Cms\\Translations' => __DIR__ . '/../..' . '/src/Cms/Translations.php', + 'Kirby\\Cms\\Url' => __DIR__ . '/../..' . '/src/Cms/Url.php', + 'Kirby\\Cms\\User' => __DIR__ . '/../..' . '/src/Cms/User.php', + 'Kirby\\Cms\\UserActions' => __DIR__ . '/../..' . '/src/Cms/UserActions.php', + 'Kirby\\Cms\\UserBlueprint' => __DIR__ . '/../..' . '/src/Cms/UserBlueprint.php', + 'Kirby\\Cms\\UserPermissions' => __DIR__ . '/../..' . '/src/Cms/UserPermissions.php', + 'Kirby\\Cms\\UserPicker' => __DIR__ . '/../..' . '/src/Cms/UserPicker.php', + 'Kirby\\Cms\\UserRules' => __DIR__ . '/../..' . '/src/Cms/UserRules.php', + 'Kirby\\Cms\\Users' => __DIR__ . '/../..' . '/src/Cms/Users.php', + 'Kirby\\Cms\\Visitor' => __DIR__ . '/../..' . '/src/Cms/Visitor.php', + 'Kirby\\ComposerInstaller\\CmsInstaller' => __DIR__ . '/..' . '/getkirby/composer-installer/src/ComposerInstaller/CmsInstaller.php', + 'Kirby\\ComposerInstaller\\Installer' => __DIR__ . '/..' . '/getkirby/composer-installer/src/ComposerInstaller/Installer.php', + 'Kirby\\ComposerInstaller\\Plugin' => __DIR__ . '/..' . '/getkirby/composer-installer/src/ComposerInstaller/Plugin.php', + 'Kirby\\ComposerInstaller\\PluginInstaller' => __DIR__ . '/..' . '/getkirby/composer-installer/src/ComposerInstaller/PluginInstaller.php', + 'Kirby\\Data\\Data' => __DIR__ . '/../..' . '/src/Data/Data.php', + 'Kirby\\Data\\Handler' => __DIR__ . '/../..' . '/src/Data/Handler.php', + 'Kirby\\Data\\Json' => __DIR__ . '/../..' . '/src/Data/Json.php', + 'Kirby\\Data\\PHP' => __DIR__ . '/../..' . '/src/Data/PHP.php', + 'Kirby\\Data\\Txt' => __DIR__ . '/../..' . '/src/Data/Txt.php', + 'Kirby\\Data\\Yaml' => __DIR__ . '/../..' . '/src/Data/Yaml.php', + 'Kirby\\Database\\Database' => __DIR__ . '/../..' . '/src/Database/Database.php', + 'Kirby\\Database\\Db' => __DIR__ . '/../..' . '/src/Database/Db.php', + 'Kirby\\Database\\Query' => __DIR__ . '/../..' . '/src/Database/Query.php', + 'Kirby\\Database\\Sql' => __DIR__ . '/../..' . '/src/Database/Sql.php', + 'Kirby\\Database\\Sql\\Mysql' => __DIR__ . '/../..' . '/src/Database/Sql/Mysql.php', + 'Kirby\\Database\\Sql\\Sqlite' => __DIR__ . '/../..' . '/src/Database/Sql/Sqlite.php', + 'Kirby\\Email\\Body' => __DIR__ . '/../..' . '/src/Email/Body.php', + 'Kirby\\Email\\Email' => __DIR__ . '/../..' . '/src/Email/Email.php', + 'Kirby\\Email\\PHPMailer' => __DIR__ . '/../..' . '/src/Email/PHPMailer.php', + 'Kirby\\Exception\\BadMethodCallException' => __DIR__ . '/../..' . '/src/Exception/BadMethodCallException.php', + 'Kirby\\Exception\\DuplicateException' => __DIR__ . '/../..' . '/src/Exception/DuplicateException.php', + 'Kirby\\Exception\\ErrorPageException' => __DIR__ . '/../..' . '/src/Exception/ErrorPageException.php', + 'Kirby\\Exception\\Exception' => __DIR__ . '/../..' . '/src/Exception/Exception.php', + 'Kirby\\Exception\\InvalidArgumentException' => __DIR__ . '/../..' . '/src/Exception/InvalidArgumentException.php', + 'Kirby\\Exception\\LogicException' => __DIR__ . '/../..' . '/src/Exception/LogicException.php', + 'Kirby\\Exception\\NotFoundException' => __DIR__ . '/../..' . '/src/Exception/NotFoundException.php', + 'Kirby\\Exception\\PermissionException' => __DIR__ . '/../..' . '/src/Exception/PermissionException.php', + 'Kirby\\Form\\Field' => __DIR__ . '/../..' . '/src/Form/Field.php', + 'Kirby\\Form\\Fields' => __DIR__ . '/../..' . '/src/Form/Fields.php', + 'Kirby\\Form\\Form' => __DIR__ . '/../..' . '/src/Form/Form.php', + 'Kirby\\Form\\Options' => __DIR__ . '/../..' . '/src/Form/Options.php', + 'Kirby\\Form\\OptionsApi' => __DIR__ . '/../..' . '/src/Form/OptionsApi.php', + 'Kirby\\Form\\OptionsQuery' => __DIR__ . '/../..' . '/src/Form/OptionsQuery.php', + 'Kirby\\Form\\Validations' => __DIR__ . '/../..' . '/src/Form/Validations.php', + 'Kirby\\Http\\Cookie' => __DIR__ . '/../..' . '/src/Http/Cookie.php', + 'Kirby\\Http\\Exceptions\\NextRouteException' => __DIR__ . '/../..' . '/src/Http/Exceptions/NextRouteException.php', + 'Kirby\\Http\\Header' => __DIR__ . '/../..' . '/src/Http/Header.php', + 'Kirby\\Http\\Idn' => __DIR__ . '/../..' . '/src/Http/Idn.php', + 'Kirby\\Http\\Params' => __DIR__ . '/../..' . '/src/Http/Params.php', + 'Kirby\\Http\\Path' => __DIR__ . '/../..' . '/src/Http/Path.php', + 'Kirby\\Http\\Query' => __DIR__ . '/../..' . '/src/Http/Query.php', + 'Kirby\\Http\\Remote' => __DIR__ . '/../..' . '/src/Http/Remote.php', + 'Kirby\\Http\\Request' => __DIR__ . '/../..' . '/src/Http/Request.php', + 'Kirby\\Http\\Request\\Auth\\BasicAuth' => __DIR__ . '/../..' . '/src/Http/Request/Auth/BasicAuth.php', + 'Kirby\\Http\\Request\\Auth\\BearerAuth' => __DIR__ . '/../..' . '/src/Http/Request/Auth/BearerAuth.php', + 'Kirby\\Http\\Request\\Body' => __DIR__ . '/../..' . '/src/Http/Request/Body.php', + 'Kirby\\Http\\Request\\Data' => __DIR__ . '/../..' . '/src/Http/Request/Data.php', + 'Kirby\\Http\\Request\\Files' => __DIR__ . '/../..' . '/src/Http/Request/Files.php', + 'Kirby\\Http\\Request\\Query' => __DIR__ . '/../..' . '/src/Http/Request/Query.php', + 'Kirby\\Http\\Response' => __DIR__ . '/../..' . '/src/Http/Response.php', + 'Kirby\\Http\\Route' => __DIR__ . '/../..' . '/src/Http/Route.php', + 'Kirby\\Http\\Router' => __DIR__ . '/../..' . '/src/Http/Router.php', + 'Kirby\\Http\\Server' => __DIR__ . '/../..' . '/src/Http/Server.php', + 'Kirby\\Http\\Uri' => __DIR__ . '/../..' . '/src/Http/Uri.php', + 'Kirby\\Http\\Url' => __DIR__ . '/../..' . '/src/Http/Url.php', + 'Kirby\\Http\\Visitor' => __DIR__ . '/../..' . '/src/Http/Visitor.php', + 'Kirby\\Image\\Camera' => __DIR__ . '/../..' . '/src/Image/Camera.php', + 'Kirby\\Image\\Darkroom' => __DIR__ . '/../..' . '/src/Image/Darkroom.php', + 'Kirby\\Image\\Darkroom\\GdLib' => __DIR__ . '/../..' . '/src/Image/Darkroom/GdLib.php', + 'Kirby\\Image\\Darkroom\\ImageMagick' => __DIR__ . '/../..' . '/src/Image/Darkroom/ImageMagick.php', + 'Kirby\\Image\\Dimensions' => __DIR__ . '/../..' . '/src/Image/Dimensions.php', + 'Kirby\\Image\\Exif' => __DIR__ . '/../..' . '/src/Image/Exif.php', + 'Kirby\\Image\\Image' => __DIR__ . '/../..' . '/src/Image/Image.php', + 'Kirby\\Image\\Location' => __DIR__ . '/../..' . '/src/Image/Location.php', + 'Kirby\\Session\\AutoSession' => __DIR__ . '/../..' . '/src/Session/AutoSession.php', + 'Kirby\\Session\\FileSessionStore' => __DIR__ . '/../..' . '/src/Session/FileSessionStore.php', + 'Kirby\\Session\\Session' => __DIR__ . '/../..' . '/src/Session/Session.php', + 'Kirby\\Session\\SessionData' => __DIR__ . '/../..' . '/src/Session/SessionData.php', + 'Kirby\\Session\\SessionStore' => __DIR__ . '/../..' . '/src/Session/SessionStore.php', + 'Kirby\\Session\\Sessions' => __DIR__ . '/../..' . '/src/Session/Sessions.php', + 'Kirby\\Text\\KirbyTag' => __DIR__ . '/../..' . '/src/Text/KirbyTag.php', + 'Kirby\\Text\\KirbyTags' => __DIR__ . '/../..' . '/src/Text/KirbyTags.php', + 'Kirby\\Text\\Markdown' => __DIR__ . '/../..' . '/src/Text/Markdown.php', + 'Kirby\\Text\\SmartyPants' => __DIR__ . '/../..' . '/src/Text/SmartyPants.php', + 'Kirby\\Toolkit\\A' => __DIR__ . '/../..' . '/src/Toolkit/A.php', + 'Kirby\\Toolkit\\Collection' => __DIR__ . '/../..' . '/src/Toolkit/Collection.php', + 'Kirby\\Toolkit\\Component' => __DIR__ . '/../..' . '/src/Toolkit/Component.php', + 'Kirby\\Toolkit\\Config' => __DIR__ . '/../..' . '/src/Toolkit/Config.php', + 'Kirby\\Toolkit\\Controller' => __DIR__ . '/../..' . '/src/Toolkit/Controller.php', + 'Kirby\\Toolkit\\Dir' => __DIR__ . '/../..' . '/src/Toolkit/Dir.php', + 'Kirby\\Toolkit\\Escape' => __DIR__ . '/../..' . '/src/Toolkit/Escape.php', + 'Kirby\\Toolkit\\F' => __DIR__ . '/../..' . '/src/Toolkit/F.php', + 'Kirby\\Toolkit\\Facade' => __DIR__ . '/../..' . '/src/Toolkit/Facade.php', + 'Kirby\\Toolkit\\File' => __DIR__ . '/../..' . '/src/Toolkit/File.php', + 'Kirby\\Toolkit\\Html' => __DIR__ . '/../..' . '/src/Toolkit/Html.php', + 'Kirby\\Toolkit\\I18n' => __DIR__ . '/../..' . '/src/Toolkit/I18n.php', + 'Kirby\\Toolkit\\Iterator' => __DIR__ . '/../..' . '/src/Toolkit/Iterator.php', + 'Kirby\\Toolkit\\Mime' => __DIR__ . '/../..' . '/src/Toolkit/Mime.php', + 'Kirby\\Toolkit\\Obj' => __DIR__ . '/../..' . '/src/Toolkit/Obj.php', + 'Kirby\\Toolkit\\Pagination' => __DIR__ . '/../..' . '/src/Toolkit/Pagination.php', + 'Kirby\\Toolkit\\Properties' => __DIR__ . '/../..' . '/src/Toolkit/Properties.php', + 'Kirby\\Toolkit\\Query' => __DIR__ . '/../..' . '/src/Toolkit/Query.php', + 'Kirby\\Toolkit\\Silo' => __DIR__ . '/../..' . '/src/Toolkit/Silo.php', + 'Kirby\\Toolkit\\Str' => __DIR__ . '/../..' . '/src/Toolkit/Str.php', + 'Kirby\\Toolkit\\Tpl' => __DIR__ . '/../..' . '/src/Toolkit/Tpl.php', + 'Kirby\\Toolkit\\V' => __DIR__ . '/../..' . '/src/Toolkit/V.php', + 'Kirby\\Toolkit\\View' => __DIR__ . '/../..' . '/src/Toolkit/View.php', + 'Kirby\\Toolkit\\Xml' => __DIR__ . '/../..' . '/src/Toolkit/Xml.php', + 'Laminas\\Escaper\\Escaper' => __DIR__ . '/..' . '/laminas/laminas-escaper/src/Escaper.php', + 'Laminas\\Escaper\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/laminas/laminas-escaper/src/Exception/ExceptionInterface.php', + 'Laminas\\Escaper\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/laminas/laminas-escaper/src/Exception/InvalidArgumentException.php', + 'Laminas\\Escaper\\Exception\\RuntimeException' => __DIR__ . '/..' . '/laminas/laminas-escaper/src/Exception/RuntimeException.php', + 'Laminas\\ZendFrameworkBridge\\Autoloader' => __DIR__ . '/..' . '/laminas/laminas-zendframework-bridge/src/Autoloader.php', + 'Laminas\\ZendFrameworkBridge\\ConfigPostProcessor' => __DIR__ . '/..' . '/laminas/laminas-zendframework-bridge/src/ConfigPostProcessor.php', + 'Laminas\\ZendFrameworkBridge\\Module' => __DIR__ . '/..' . '/laminas/laminas-zendframework-bridge/src/Module.php', + 'Laminas\\ZendFrameworkBridge\\Replacements' => __DIR__ . '/..' . '/laminas/laminas-zendframework-bridge/src/Replacements.php', + 'Laminas\\ZendFrameworkBridge\\RewriteRules' => __DIR__ . '/..' . '/laminas/laminas-zendframework-bridge/src/RewriteRules.php', + 'League\\ColorExtractor\\Color' => __DIR__ . '/..' . '/league/color-extractor/src/League/ColorExtractor/Color.php', + 'League\\ColorExtractor\\ColorExtractor' => __DIR__ . '/..' . '/league/color-extractor/src/League/ColorExtractor/ColorExtractor.php', + 'League\\ColorExtractor\\Palette' => __DIR__ . '/..' . '/league/color-extractor/src/League/ColorExtractor/Palette.php', + 'Michelf\\SmartyPants' => __DIR__ . '/..' . '/michelf/php-smartypants/Michelf/SmartyPants.php', + 'Michelf\\SmartyPantsTypographer' => __DIR__ . '/..' . '/michelf/php-smartypants/Michelf/SmartyPantsTypographer.php', + 'PHPMailer\\PHPMailer\\Exception' => __DIR__ . '/..' . '/phpmailer/phpmailer/src/Exception.php', + 'PHPMailer\\PHPMailer\\OAuth' => __DIR__ . '/..' . '/phpmailer/phpmailer/src/OAuth.php', + 'PHPMailer\\PHPMailer\\PHPMailer' => __DIR__ . '/..' . '/phpmailer/phpmailer/src/PHPMailer.php', + 'PHPMailer\\PHPMailer\\POP3' => __DIR__ . '/..' . '/phpmailer/phpmailer/src/POP3.php', + 'PHPMailer\\PHPMailer\\SMTP' => __DIR__ . '/..' . '/phpmailer/phpmailer/src/SMTP.php', + 'Parsedown' => __DIR__ . '/../..' . '/dependencies/parsedown/Parsedown.php', + 'ParsedownExtra' => __DIR__ . '/../..' . '/dependencies/parsedown-extra/ParsedownExtra.php', + 'Psr\\Log\\AbstractLogger' => __DIR__ . '/..' . '/psr/log/Psr/Log/AbstractLogger.php', + 'Psr\\Log\\InvalidArgumentException' => __DIR__ . '/..' . '/psr/log/Psr/Log/InvalidArgumentException.php', + 'Psr\\Log\\LogLevel' => __DIR__ . '/..' . '/psr/log/Psr/Log/LogLevel.php', + 'Psr\\Log\\LoggerAwareInterface' => __DIR__ . '/..' . '/psr/log/Psr/Log/LoggerAwareInterface.php', + 'Psr\\Log\\LoggerAwareTrait' => __DIR__ . '/..' . '/psr/log/Psr/Log/LoggerAwareTrait.php', + 'Psr\\Log\\LoggerInterface' => __DIR__ . '/..' . '/psr/log/Psr/Log/LoggerInterface.php', + 'Psr\\Log\\LoggerTrait' => __DIR__ . '/..' . '/psr/log/Psr/Log/LoggerTrait.php', + 'Psr\\Log\\NullLogger' => __DIR__ . '/..' . '/psr/log/Psr/Log/NullLogger.php', + 'Psr\\Log\\Test\\DummyTest' => __DIR__ . '/..' . '/psr/log/Psr/Log/Test/LoggerInterfaceTest.php', + 'Psr\\Log\\Test\\LoggerInterfaceTest' => __DIR__ . '/..' . '/psr/log/Psr/Log/Test/LoggerInterfaceTest.php', + 'Psr\\Log\\Test\\TestLogger' => __DIR__ . '/..' . '/psr/log/Psr/Log/Test/TestLogger.php', + 'Symfony\\Polyfill\\Mbstring\\Mbstring' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/Mbstring.php', + 'TrueBV\\Exception\\DomainOutOfBoundsException' => __DIR__ . '/..' . '/true/punycode/src/Exception/DomainOutOfBoundsException.php', + 'TrueBV\\Exception\\LabelOutOfBoundsException' => __DIR__ . '/..' . '/true/punycode/src/Exception/LabelOutOfBoundsException.php', + 'TrueBV\\Exception\\OutOfBoundsException' => __DIR__ . '/..' . '/true/punycode/src/Exception/OutOfBoundsException.php', + 'TrueBV\\Punycode' => __DIR__ . '/..' . '/true/punycode/src/Punycode.php', + 'Whoops\\Exception\\ErrorException' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Exception/ErrorException.php', + 'Whoops\\Exception\\Formatter' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Exception/Formatter.php', + 'Whoops\\Exception\\Frame' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Exception/Frame.php', + 'Whoops\\Exception\\FrameCollection' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Exception/FrameCollection.php', + 'Whoops\\Exception\\Inspector' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Exception/Inspector.php', + 'Whoops\\Handler\\CallbackHandler' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Handler/CallbackHandler.php', + 'Whoops\\Handler\\Handler' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Handler/Handler.php', + 'Whoops\\Handler\\HandlerInterface' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Handler/HandlerInterface.php', + 'Whoops\\Handler\\JsonResponseHandler' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Handler/JsonResponseHandler.php', + 'Whoops\\Handler\\PlainTextHandler' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Handler/PlainTextHandler.php', + 'Whoops\\Handler\\PrettyPageHandler' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Handler/PrettyPageHandler.php', + 'Whoops\\Handler\\XmlResponseHandler' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Handler/XmlResponseHandler.php', + 'Whoops\\Run' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Run.php', + 'Whoops\\RunInterface' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/RunInterface.php', + 'Whoops\\Util\\HtmlDumperOutput' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Util/HtmlDumperOutput.php', + 'Whoops\\Util\\Misc' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Util/Misc.php', + 'Whoops\\Util\\SystemFacade' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Util/SystemFacade.php', + 'Whoops\\Util\\TemplateHelper' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Util/TemplateHelper.php', + 'claviska\\SimpleImage' => __DIR__ . '/..' . '/claviska/simpleimage/src/claviska/SimpleImage.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit12091bebabd81c9aba88b2aeec22c8d7::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit12091bebabd81c9aba88b2aeec22c8d7::$prefixDirsPsr4; + $loader->fallbackDirsPsr4 = ComposerStaticInit12091bebabd81c9aba88b2aeec22c8d7::$fallbackDirsPsr4; + $loader->prefixesPsr0 = ComposerStaticInit12091bebabd81c9aba88b2aeec22c8d7::$prefixesPsr0; + $loader->classMap = ComposerStaticInit12091bebabd81c9aba88b2aeec22c8d7::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/kirby/vendor/filp/whoops/src/Whoops/Exception/ErrorException.php b/kirby/vendor/filp/whoops/src/Whoops/Exception/ErrorException.php new file mode 100755 index 0000000..d74e823 --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Exception/ErrorException.php @@ -0,0 +1,17 @@ + + */ + +namespace Whoops\Exception; + +use ErrorException as BaseErrorException; + +/** + * Wraps ErrorException; mostly used for typing (at least now) + * to easily cleanup the stack trace of redundant info. + */ +class ErrorException extends BaseErrorException +{ +} diff --git a/kirby/vendor/filp/whoops/src/Whoops/Exception/Formatter.php b/kirby/vendor/filp/whoops/src/Whoops/Exception/Formatter.php new file mode 100755 index 0000000..e467559 --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Exception/Formatter.php @@ -0,0 +1,73 @@ + + */ + +namespace Whoops\Exception; + +class Formatter +{ + /** + * Returns all basic information about the exception in a simple array + * for further convertion to other languages + * @param Inspector $inspector + * @param bool $shouldAddTrace + * @return array + */ + public static function formatExceptionAsDataArray(Inspector $inspector, $shouldAddTrace) + { + $exception = $inspector->getException(); + $response = [ + 'type' => get_class($exception), + 'message' => $exception->getMessage(), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + ]; + + if ($shouldAddTrace) { + $frames = $inspector->getFrames(); + $frameData = []; + + foreach ($frames as $frame) { + /** @var Frame $frame */ + $frameData[] = [ + 'file' => $frame->getFile(), + 'line' => $frame->getLine(), + 'function' => $frame->getFunction(), + 'class' => $frame->getClass(), + 'args' => $frame->getArgs(), + ]; + } + + $response['trace'] = $frameData; + } + + return $response; + } + + public static function formatExceptionPlain(Inspector $inspector) + { + $message = $inspector->getException()->getMessage(); + $frames = $inspector->getFrames(); + + $plain = $inspector->getExceptionName(); + $plain .= ' thrown with message "'; + $plain .= $message; + $plain .= '"'."\n\n"; + + $plain .= "Stacktrace:\n"; + foreach ($frames as $i => $frame) { + $plain .= "#". (count($frames) - $i - 1). " "; + $plain .= $frame->getClass() ?: ''; + $plain .= $frame->getClass() && $frame->getFunction() ? ":" : ""; + $plain .= $frame->getFunction() ?: ''; + $plain .= ' in '; + $plain .= ($frame->getFile() ?: '<#unknown>'); + $plain .= ':'; + $plain .= (int) $frame->getLine(). "\n"; + } + + return $plain; + } +} diff --git a/kirby/vendor/filp/whoops/src/Whoops/Exception/Frame.php b/kirby/vendor/filp/whoops/src/Whoops/Exception/Frame.php new file mode 100755 index 0000000..0aad546 --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Exception/Frame.php @@ -0,0 +1,291 @@ + + */ + +namespace Whoops\Exception; + +use InvalidArgumentException; +use Serializable; + +class Frame implements Serializable +{ + /** + * @var array + */ + protected $frame; + + /** + * @var string + */ + protected $fileContentsCache; + + /** + * @var array[] + */ + protected $comments = []; + + /** + * @var bool + */ + protected $application; + + /** + * @param array[] + */ + public function __construct(array $frame) + { + $this->frame = $frame; + } + + /** + * @param bool $shortened + * @return string|null + */ + public function getFile($shortened = false) + { + if (empty($this->frame['file'])) { + return null; + } + + $file = $this->frame['file']; + + // Check if this frame occurred within an eval(). + // @todo: This can be made more reliable by checking if we've entered + // eval() in a previous trace, but will need some more work on the upper + // trace collector(s). + if (preg_match('/^(.*)\((\d+)\) : (?:eval\(\)\'d|assert) code$/', $file, $matches)) { + $file = $this->frame['file'] = $matches[1]; + $this->frame['line'] = (int) $matches[2]; + } + + if ($shortened && is_string($file)) { + // Replace the part of the path that all frames have in common, and add 'soft hyphens' for smoother line-breaks. + $dirname = dirname(dirname(dirname(dirname(dirname(dirname(__DIR__)))))); + if ($dirname !== '/') { + $file = str_replace($dirname, "…", $file); + } + $file = str_replace("/", "/­", $file); + } + + return $file; + } + + /** + * @return int|null + */ + public function getLine() + { + return isset($this->frame['line']) ? $this->frame['line'] : null; + } + + /** + * @return string|null + */ + public function getClass() + { + return isset($this->frame['class']) ? $this->frame['class'] : null; + } + + /** + * @return string|null + */ + public function getFunction() + { + return isset($this->frame['function']) ? $this->frame['function'] : null; + } + + /** + * @return array + */ + public function getArgs() + { + return isset($this->frame['args']) ? (array) $this->frame['args'] : []; + } + + /** + * Returns the full contents of the file for this frame, + * if it's known. + * @return string|null + */ + public function getFileContents() + { + if ($this->fileContentsCache === null && $filePath = $this->getFile()) { + // Leave the stage early when 'Unknown' or '[internal]' is passed + // this would otherwise raise an exception when + // open_basedir is enabled. + if ($filePath === "Unknown" || $filePath === '[internal]') { + return null; + } + + $this->fileContentsCache = file_get_contents($filePath); + } + + return $this->fileContentsCache; + } + + /** + * Adds a comment to this frame, that can be received and + * used by other handlers. For example, the PrettyPage handler + * can attach these comments under the code for each frame. + * + * An interesting use for this would be, for example, code analysis + * & annotations. + * + * @param string $comment + * @param string $context Optional string identifying the origin of the comment + */ + public function addComment($comment, $context = 'global') + { + $this->comments[] = [ + 'comment' => $comment, + 'context' => $context, + ]; + } + + /** + * Returns all comments for this frame. Optionally allows + * a filter to only retrieve comments from a specific + * context. + * + * @param string $filter + * @return array[] + */ + public function getComments($filter = null) + { + $comments = $this->comments; + + if ($filter !== null) { + $comments = array_filter($comments, function ($c) use ($filter) { + return $c['context'] == $filter; + }); + } + + return $comments; + } + + /** + * Returns the array containing the raw frame data from which + * this Frame object was built + * + * @return array + */ + public function getRawFrame() + { + return $this->frame; + } + + /** + * Returns the contents of the file for this frame as an + * array of lines, and optionally as a clamped range of lines. + * + * NOTE: lines are 0-indexed + * + * @example + * Get all lines for this file + * $frame->getFileLines(); // => array( 0 => ' '...', ...) + * @example + * Get one line for this file, starting at line 10 (zero-indexed, remember!) + * $frame->getFileLines(9, 1); // array( 10 => '...', 11 => '...') + * + * @throws InvalidArgumentException if $length is less than or equal to 0 + * @param int $start + * @param int $length + * @return string[]|null + */ + public function getFileLines($start = 0, $length = null) + { + if (null !== ($contents = $this->getFileContents())) { + $lines = explode("\n", $contents); + + // Get a subset of lines from $start to $end + if ($length !== null) { + $start = (int) $start; + $length = (int) $length; + if ($start < 0) { + $start = 0; + } + + if ($length <= 0) { + throw new InvalidArgumentException( + "\$length($length) cannot be lower or equal to 0" + ); + } + + $lines = array_slice($lines, $start, $length, true); + } + + return $lines; + } + } + + /** + * Implements the Serializable interface, with special + * steps to also save the existing comments. + * + * @see Serializable::serialize + * @return string + */ + public function serialize() + { + $frame = $this->frame; + if (!empty($this->comments)) { + $frame['_comments'] = $this->comments; + } + + return serialize($frame); + } + + /** + * Unserializes the frame data, while also preserving + * any existing comment data. + * + * @see Serializable::unserialize + * @param string $serializedFrame + */ + public function unserialize($serializedFrame) + { + $frame = unserialize($serializedFrame); + + if (!empty($frame['_comments'])) { + $this->comments = $frame['_comments']; + unset($frame['_comments']); + } + + $this->frame = $frame; + } + + /** + * Compares Frame against one another + * @param Frame $frame + * @return bool + */ + public function equals(Frame $frame) + { + if (!$this->getFile() || $this->getFile() === 'Unknown' || !$this->getLine()) { + return false; + } + return $frame->getFile() === $this->getFile() && $frame->getLine() === $this->getLine(); + } + + /** + * Returns whether this frame belongs to the application or not. + * + * @return boolean + */ + public function isApplication() + { + return $this->application; + } + + /** + * Mark as an frame belonging to the application. + * + * @param boolean $application + */ + public function setApplication($application) + { + $this->application = $application; + } +} diff --git a/kirby/vendor/filp/whoops/src/Whoops/Exception/FrameCollection.php b/kirby/vendor/filp/whoops/src/Whoops/Exception/FrameCollection.php new file mode 100755 index 0000000..b043a1c --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Exception/FrameCollection.php @@ -0,0 +1,203 @@ + + */ + +namespace Whoops\Exception; + +use ArrayAccess; +use ArrayIterator; +use Countable; +use IteratorAggregate; +use Serializable; +use UnexpectedValueException; + +/** + * Exposes a fluent interface for dealing with an ordered list + * of stack-trace frames. + */ +class FrameCollection implements ArrayAccess, IteratorAggregate, Serializable, Countable +{ + /** + * @var array[] + */ + private $frames; + + /** + * @param array $frames + */ + public function __construct(array $frames) + { + $this->frames = array_map(function ($frame) { + return new Frame($frame); + }, $frames); + } + + /** + * Filters frames using a callable, returns the same FrameCollection + * + * @param callable $callable + * @return FrameCollection + */ + public function filter($callable) + { + $this->frames = array_values(array_filter($this->frames, $callable)); + return $this; + } + + /** + * Map the collection of frames + * + * @param callable $callable + * @return FrameCollection + */ + public function map($callable) + { + // Contain the map within a higher-order callable + // that enforces type-correctness for the $callable + $this->frames = array_map(function ($frame) use ($callable) { + $frame = call_user_func($callable, $frame); + + if (!$frame instanceof Frame) { + throw new UnexpectedValueException( + "Callable to " . __METHOD__ . " must return a Frame object" + ); + } + + return $frame; + }, $this->frames); + + return $this; + } + + /** + * Returns an array with all frames, does not affect + * the internal array. + * + * @todo If this gets any more complex than this, + * have getIterator use this method. + * @see FrameCollection::getIterator + * @return array + */ + public function getArray() + { + return $this->frames; + } + + /** + * @see IteratorAggregate::getIterator + * @return ArrayIterator + */ + public function getIterator() + { + return new ArrayIterator($this->frames); + } + + /** + * @see ArrayAccess::offsetExists + * @param int $offset + */ + public function offsetExists($offset) + { + return isset($this->frames[$offset]); + } + + /** + * @see ArrayAccess::offsetGet + * @param int $offset + */ + public function offsetGet($offset) + { + return $this->frames[$offset]; + } + + /** + * @see ArrayAccess::offsetSet + * @param int $offset + */ + public function offsetSet($offset, $value) + { + throw new \Exception(__CLASS__ . ' is read only'); + } + + /** + * @see ArrayAccess::offsetUnset + * @param int $offset + */ + public function offsetUnset($offset) + { + throw new \Exception(__CLASS__ . ' is read only'); + } + + /** + * @see Countable::count + * @return int + */ + public function count() + { + return count($this->frames); + } + + /** + * Count the frames that belongs to the application. + * + * @return int + */ + public function countIsApplication() + { + return count(array_filter($this->frames, function (Frame $f) { + return $f->isApplication(); + })); + } + + /** + * @see Serializable::serialize + * @return string + */ + public function serialize() + { + return serialize($this->frames); + } + + /** + * @see Serializable::unserialize + * @param string $serializedFrames + */ + public function unserialize($serializedFrames) + { + $this->frames = unserialize($serializedFrames); + } + + /** + * @param Frame[] $frames Array of Frame instances, usually from $e->getPrevious() + */ + public function prependFrames(array $frames) + { + $this->frames = array_merge($frames, $this->frames); + } + + /** + * Gets the innermost part of stack trace that is not the same as that of outer exception + * + * @param FrameCollection $parentFrames Outer exception frames to compare tail against + * @return Frame[] + */ + public function topDiff(FrameCollection $parentFrames) + { + $diff = $this->frames; + + $parentFrames = $parentFrames->getArray(); + $p = count($parentFrames)-1; + + for ($i = count($diff)-1; $i >= 0 && $p >= 0; $i--) { + /** @var Frame $tailFrame */ + $tailFrame = $diff[$i]; + if ($tailFrame->equals($parentFrames[$p])) { + unset($diff[$i]); + } + $p--; + } + return $diff; + } +} diff --git a/kirby/vendor/filp/whoops/src/Whoops/Exception/Inspector.php b/kirby/vendor/filp/whoops/src/Whoops/Exception/Inspector.php new file mode 100755 index 0000000..96cb9b5 --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Exception/Inspector.php @@ -0,0 +1,323 @@ + + */ + +namespace Whoops\Exception; + +use Whoops\Util\Misc; + +class Inspector +{ + /** + * @var \Throwable + */ + private $exception; + + /** + * @var \Whoops\Exception\FrameCollection + */ + private $frames; + + /** + * @var \Whoops\Exception\Inspector + */ + private $previousExceptionInspector; + + /** + * @var \Throwable[] + */ + private $previousExceptions; + + /** + * @param \Throwable $exception The exception to inspect + */ + public function __construct($exception) + { + $this->exception = $exception; + } + + /** + * @return \Throwable + */ + public function getException() + { + return $this->exception; + } + + /** + * @return string + */ + public function getExceptionName() + { + return get_class($this->exception); + } + + /** + * @return string + */ + public function getExceptionMessage() + { + return $this->extractDocrefUrl($this->exception->getMessage())['message']; + } + + /** + * @return string[] + */ + public function getPreviousExceptionMessages() + { + return array_map(function ($prev) { + /** @var \Throwable $prev */ + return $this->extractDocrefUrl($prev->getMessage())['message']; + }, $this->getPreviousExceptions()); + } + + /** + * @return int[] + */ + public function getPreviousExceptionCodes() + { + return array_map(function ($prev) { + /** @var \Throwable $prev */ + return $prev->getCode(); + }, $this->getPreviousExceptions()); + } + + /** + * Returns a url to the php-manual related to the underlying error - when available. + * + * @return string|null + */ + public function getExceptionDocrefUrl() + { + return $this->extractDocrefUrl($this->exception->getMessage())['url']; + } + + private function extractDocrefUrl($message) + { + $docref = [ + 'message' => $message, + 'url' => null, + ]; + + // php embbeds urls to the manual into the Exception message with the following ini-settings defined + // http://php.net/manual/en/errorfunc.configuration.php#ini.docref-root + if (!ini_get('html_errors') || !ini_get('docref_root')) { + return $docref; + } + + $pattern = "/\[(?:[^<]+)<\/a>\]/"; + if (preg_match($pattern, $message, $matches)) { + // -> strip those automatically generated links from the exception message + $docref['message'] = preg_replace($pattern, '', $message, 1); + $docref['url'] = $matches[1]; + } + + return $docref; + } + + /** + * Does the wrapped Exception has a previous Exception? + * @return bool + */ + public function hasPreviousException() + { + return $this->previousExceptionInspector || $this->exception->getPrevious(); + } + + /** + * Returns an Inspector for a previous Exception, if any. + * @todo Clean this up a bit, cache stuff a bit better. + * @return Inspector + */ + public function getPreviousExceptionInspector() + { + if ($this->previousExceptionInspector === null) { + $previousException = $this->exception->getPrevious(); + + if ($previousException) { + $this->previousExceptionInspector = new Inspector($previousException); + } + } + + return $this->previousExceptionInspector; + } + + + /** + * Returns an array of all previous exceptions for this inspector's exception + * @return \Throwable[] + */ + public function getPreviousExceptions() + { + if ($this->previousExceptions === null) { + $this->previousExceptions = []; + + $prev = $this->exception->getPrevious(); + while ($prev !== null) { + $this->previousExceptions[] = $prev; + $prev = $prev->getPrevious(); + } + } + + return $this->previousExceptions; + } + + /** + * Returns an iterator for the inspected exception's + * frames. + * @return \Whoops\Exception\FrameCollection + */ + public function getFrames() + { + if ($this->frames === null) { + $frames = $this->getTrace($this->exception); + + // Fill empty line/file info for call_user_func_array usages (PHP Bug #44428) + foreach ($frames as $k => $frame) { + if (empty($frame['file'])) { + // Default values when file and line are missing + $file = '[internal]'; + $line = 0; + + $next_frame = !empty($frames[$k + 1]) ? $frames[$k + 1] : []; + + if ($this->isValidNextFrame($next_frame)) { + $file = $next_frame['file']; + $line = $next_frame['line']; + } + + $frames[$k]['file'] = $file; + $frames[$k]['line'] = $line; + } + } + + // Find latest non-error handling frame index ($i) used to remove error handling frames + $i = 0; + foreach ($frames as $k => $frame) { + if ($frame['file'] == $this->exception->getFile() && $frame['line'] == $this->exception->getLine()) { + $i = $k; + } + } + + // Remove error handling frames + if ($i > 0) { + array_splice($frames, 0, $i); + } + + $firstFrame = $this->getFrameFromException($this->exception); + array_unshift($frames, $firstFrame); + + $this->frames = new FrameCollection($frames); + + if ($previousInspector = $this->getPreviousExceptionInspector()) { + // Keep outer frame on top of the inner one + $outerFrames = $this->frames; + $newFrames = clone $previousInspector->getFrames(); + // I assume it will always be set, but let's be safe + if (isset($newFrames[0])) { + $newFrames[0]->addComment( + $previousInspector->getExceptionMessage(), + 'Exception message:' + ); + } + $newFrames->prependFrames($outerFrames->topDiff($newFrames)); + $this->frames = $newFrames; + } + } + + return $this->frames; + } + + /** + * Gets the backtrace from an exception. + * + * If xdebug is installed + * + * @param \Throwable $e + * @return array + */ + protected function getTrace($e) + { + $traces = $e->getTrace(); + + // Get trace from xdebug if enabled, failure exceptions only trace to the shutdown handler by default + if (!$e instanceof \ErrorException) { + return $traces; + } + + if (!Misc::isLevelFatal($e->getSeverity())) { + return $traces; + } + + if (!extension_loaded('xdebug') || !xdebug_is_enabled()) { + return []; + } + + // Use xdebug to get the full stack trace and remove the shutdown handler stack trace + $stack = array_reverse(xdebug_get_function_stack()); + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + $traces = array_diff_key($stack, $trace); + + return $traces; + } + + /** + * Given an exception, generates an array in the format + * generated by Exception::getTrace() + * @param \Throwable $exception + * @return array + */ + protected function getFrameFromException($exception) + { + return [ + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'class' => get_class($exception), + 'args' => [ + $exception->getMessage(), + ], + ]; + } + + /** + * Given an error, generates an array in the format + * generated by ErrorException + * @param ErrorException $exception + * @return array + */ + protected function getFrameFromError(ErrorException $exception) + { + return [ + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'class' => null, + 'args' => [], + ]; + } + + /** + * Determine if the frame can be used to fill in previous frame's missing info + * happens for call_user_func and call_user_func_array usages (PHP Bug #44428) + * + * @param array $frame + * @return bool + */ + protected function isValidNextFrame(array $frame) + { + if (empty($frame['file'])) { + return false; + } + + if (empty($frame['line'])) { + return false; + } + + if (empty($frame['function']) || !stristr($frame['function'], 'call_user_func')) { + return false; + } + + return true; + } +} diff --git a/kirby/vendor/filp/whoops/src/Whoops/Handler/CallbackHandler.php b/kirby/vendor/filp/whoops/src/Whoops/Handler/CallbackHandler.php new file mode 100755 index 0000000..cc46e70 --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Handler/CallbackHandler.php @@ -0,0 +1,52 @@ + + */ + +namespace Whoops\Handler; + +use InvalidArgumentException; + +/** + * Wrapper for Closures passed as handlers. Can be used + * directly, or will be instantiated automagically by Whoops\Run + * if passed to Run::pushHandler + */ +class CallbackHandler extends Handler +{ + /** + * @var callable + */ + protected $callable; + + /** + * @throws InvalidArgumentException If argument is not callable + * @param callable $callable + */ + public function __construct($callable) + { + if (!is_callable($callable)) { + throw new InvalidArgumentException( + 'Argument to ' . __METHOD__ . ' must be valid callable' + ); + } + + $this->callable = $callable; + } + + /** + * @return int|null + */ + public function handle() + { + $exception = $this->getException(); + $inspector = $this->getInspector(); + $run = $this->getRun(); + $callable = $this->callable; + + // invoke the callable directly, to get simpler stacktraces (in comparison to call_user_func). + // this assumes that $callable is a properly typed php-callable, which we check in __construct(). + return $callable($exception, $inspector, $run); + } +} diff --git a/kirby/vendor/filp/whoops/src/Whoops/Handler/Handler.php b/kirby/vendor/filp/whoops/src/Whoops/Handler/Handler.php new file mode 100755 index 0000000..cf1f708 --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Handler/Handler.php @@ -0,0 +1,95 @@ + + */ + +namespace Whoops\Handler; + +use Whoops\Exception\Inspector; +use Whoops\RunInterface; + +/** + * Abstract implementation of a Handler. + */ +abstract class Handler implements HandlerInterface +{ + /* + Return constants that can be returned from Handler::handle + to message the handler walker. + */ + const DONE = 0x10; // returning this is optional, only exists for + // semantic purposes + /** + * The Handler has handled the Throwable in some way, and wishes to skip any other Handler. + * Execution will continue. + */ + const LAST_HANDLER = 0x20; + /** + * The Handler has handled the Throwable in some way, and wishes to quit/stop execution + */ + const QUIT = 0x30; + + /** + * @var RunInterface + */ + private $run; + + /** + * @var Inspector $inspector + */ + private $inspector; + + /** + * @var \Throwable $exception + */ + private $exception; + + /** + * @param RunInterface $run + */ + public function setRun(RunInterface $run) + { + $this->run = $run; + } + + /** + * @return RunInterface + */ + protected function getRun() + { + return $this->run; + } + + /** + * @param Inspector $inspector + */ + public function setInspector(Inspector $inspector) + { + $this->inspector = $inspector; + } + + /** + * @return Inspector + */ + protected function getInspector() + { + return $this->inspector; + } + + /** + * @param \Throwable $exception + */ + public function setException($exception) + { + $this->exception = $exception; + } + + /** + * @return \Throwable + */ + protected function getException() + { + return $this->exception; + } +} diff --git a/kirby/vendor/filp/whoops/src/Whoops/Handler/HandlerInterface.php b/kirby/vendor/filp/whoops/src/Whoops/Handler/HandlerInterface.php new file mode 100755 index 0000000..0265a85 --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Handler/HandlerInterface.php @@ -0,0 +1,36 @@ + + */ + +namespace Whoops\Handler; + +use Whoops\Exception\Inspector; +use Whoops\RunInterface; + +interface HandlerInterface +{ + /** + * @return int|null A handler may return nothing, or a Handler::HANDLE_* constant + */ + public function handle(); + + /** + * @param RunInterface $run + * @return void + */ + public function setRun(RunInterface $run); + + /** + * @param \Throwable $exception + * @return void + */ + public function setException($exception); + + /** + * @param Inspector $inspector + * @return void + */ + public function setInspector(Inspector $inspector); +} diff --git a/kirby/vendor/filp/whoops/src/Whoops/Handler/JsonResponseHandler.php b/kirby/vendor/filp/whoops/src/Whoops/Handler/JsonResponseHandler.php new file mode 100755 index 0000000..fdd7ead --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Handler/JsonResponseHandler.php @@ -0,0 +1,88 @@ + + */ + +namespace Whoops\Handler; + +use Whoops\Exception\Formatter; + +/** + * Catches an exception and converts it to a JSON + * response. Additionally can also return exception + * frames for consumption by an API. + */ +class JsonResponseHandler extends Handler +{ + /** + * @var bool + */ + private $returnFrames = false; + + /** + * @var bool + */ + private $jsonApi = false; + + /** + * Returns errors[[]] instead of error[] to be in compliance with the json:api spec + * @param bool $jsonApi Default is false + * @return $this + */ + public function setJsonApi($jsonApi = false) + { + $this->jsonApi = (bool) $jsonApi; + return $this; + } + + /** + * @param bool|null $returnFrames + * @return bool|$this + */ + public function addTraceToOutput($returnFrames = null) + { + if (func_num_args() == 0) { + return $this->returnFrames; + } + + $this->returnFrames = (bool) $returnFrames; + return $this; + } + + /** + * @return int + */ + public function handle() + { + if ($this->jsonApi === true) { + $response = [ + 'errors' => [ + Formatter::formatExceptionAsDataArray( + $this->getInspector(), + $this->addTraceToOutput() + ), + ] + ]; + } else { + $response = [ + 'error' => Formatter::formatExceptionAsDataArray( + $this->getInspector(), + $this->addTraceToOutput() + ), + ]; + } + + echo json_encode($response, defined('JSON_PARTIAL_OUTPUT_ON_ERROR') ? JSON_PARTIAL_OUTPUT_ON_ERROR : 0); + + return Handler::QUIT; + } + + /** + * @return string + */ + public function contentType() + { + return 'application/json'; + } +} diff --git a/kirby/vendor/filp/whoops/src/Whoops/Handler/PlainTextHandler.php b/kirby/vendor/filp/whoops/src/Whoops/Handler/PlainTextHandler.php new file mode 100755 index 0000000..2f5be90 --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Handler/PlainTextHandler.php @@ -0,0 +1,314 @@ + +* Plaintext handler for command line and logs. +* @author Pierre-Yves Landuré +*/ + +namespace Whoops\Handler; + +use InvalidArgumentException; +use Psr\Log\LoggerInterface; +use Whoops\Exception\Frame; + +/** +* Handler outputing plaintext error messages. Can be used +* directly, or will be instantiated automagically by Whoops\Run +* if passed to Run::pushHandler +*/ +class PlainTextHandler extends Handler +{ + const VAR_DUMP_PREFIX = ' | '; + + /** + * @var \Psr\Log\LoggerInterface + */ + protected $logger; + + /** + * @var callable + */ + protected $dumper; + + /** + * @var bool + */ + private $addTraceToOutput = true; + + /** + * @var bool|integer + */ + private $addTraceFunctionArgsToOutput = false; + + /** + * @var integer + */ + private $traceFunctionArgsOutputLimit = 1024; + + /** + * @var bool + */ + private $loggerOnly = false; + + /** + * Constructor. + * @throws InvalidArgumentException If argument is not null or a LoggerInterface + * @param \Psr\Log\LoggerInterface|null $logger + */ + public function __construct($logger = null) + { + $this->setLogger($logger); + } + + /** + * Set the output logger interface. + * @throws InvalidArgumentException If argument is not null or a LoggerInterface + * @param \Psr\Log\LoggerInterface|null $logger + */ + public function setLogger($logger = null) + { + if (! (is_null($logger) + || $logger instanceof LoggerInterface)) { + throw new InvalidArgumentException( + 'Argument to ' . __METHOD__ . + " must be a valid Logger Interface (aka. Monolog), " . + get_class($logger) . ' given.' + ); + } + + $this->logger = $logger; + } + + /** + * @return \Psr\Log\LoggerInterface|null + */ + public function getLogger() + { + return $this->logger; + } + + /** + * Set var dumper callback function. + * + * @param callable $dumper + * @return void + */ + public function setDumper(callable $dumper) + { + $this->dumper = $dumper; + } + + /** + * Add error trace to output. + * @param bool|null $addTraceToOutput + * @return bool|$this + */ + public function addTraceToOutput($addTraceToOutput = null) + { + if (func_num_args() == 0) { + return $this->addTraceToOutput; + } + + $this->addTraceToOutput = (bool) $addTraceToOutput; + return $this; + } + + /** + * Add error trace function arguments to output. + * Set to True for all frame args, or integer for the n first frame args. + * @param bool|integer|null $addTraceFunctionArgsToOutput + * @return null|bool|integer + */ + public function addTraceFunctionArgsToOutput($addTraceFunctionArgsToOutput = null) + { + if (func_num_args() == 0) { + return $this->addTraceFunctionArgsToOutput; + } + + if (! is_integer($addTraceFunctionArgsToOutput)) { + $this->addTraceFunctionArgsToOutput = (bool) $addTraceFunctionArgsToOutput; + } else { + $this->addTraceFunctionArgsToOutput = $addTraceFunctionArgsToOutput; + } + } + + /** + * Set the size limit in bytes of frame arguments var_dump output. + * If the limit is reached, the var_dump output is discarded. + * Prevent memory limit errors. + * @var integer + */ + public function setTraceFunctionArgsOutputLimit($traceFunctionArgsOutputLimit) + { + $this->traceFunctionArgsOutputLimit = (integer) $traceFunctionArgsOutputLimit; + } + + /** + * Create plain text response and return it as a string + * @return string + */ + public function generateResponse() + { + $exception = $this->getException(); + return sprintf( + "%s: %s in file %s on line %d%s\n", + get_class($exception), + $exception->getMessage(), + $exception->getFile(), + $exception->getLine(), + $this->getTraceOutput() + ); + } + + /** + * Get the size limit in bytes of frame arguments var_dump output. + * If the limit is reached, the var_dump output is discarded. + * Prevent memory limit errors. + * @return integer + */ + public function getTraceFunctionArgsOutputLimit() + { + return $this->traceFunctionArgsOutputLimit; + } + + /** + * Only output to logger. + * @param bool|null $loggerOnly + * @return null|bool + */ + public function loggerOnly($loggerOnly = null) + { + if (func_num_args() == 0) { + return $this->loggerOnly; + } + + $this->loggerOnly = (bool) $loggerOnly; + } + + /** + * Test if handler can output to stdout. + * @return bool + */ + private function canOutput() + { + return !$this->loggerOnly(); + } + + /** + * Get the frame args var_dump. + * @param \Whoops\Exception\Frame $frame [description] + * @param integer $line [description] + * @return string + */ + private function getFrameArgsOutput(Frame $frame, $line) + { + if ($this->addTraceFunctionArgsToOutput() === false + || $this->addTraceFunctionArgsToOutput() < $line) { + return ''; + } + + // Dump the arguments: + ob_start(); + $this->dump($frame->getArgs()); + if (ob_get_length() > $this->getTraceFunctionArgsOutputLimit()) { + // The argument var_dump is to big. + // Discarded to limit memory usage. + ob_clean(); + return sprintf( + "\n%sArguments dump length greater than %d Bytes. Discarded.", + self::VAR_DUMP_PREFIX, + $this->getTraceFunctionArgsOutputLimit() + ); + } + + return sprintf( + "\n%s", + preg_replace('/^/m', self::VAR_DUMP_PREFIX, ob_get_clean()) + ); + } + + /** + * Dump variable. + * + * @param mixed $var + * @return void + */ + protected function dump($var) + { + if ($this->dumper) { + call_user_func($this->dumper, $var); + } else { + var_dump($var); + } + } + + /** + * Get the exception trace as plain text. + * @return string + */ + private function getTraceOutput() + { + if (! $this->addTraceToOutput()) { + return ''; + } + $inspector = $this->getInspector(); + $frames = $inspector->getFrames(); + + $response = "\nStack trace:"; + + $line = 1; + foreach ($frames as $frame) { + /** @var Frame $frame */ + $class = $frame->getClass(); + + $template = "\n%3d. %s->%s() %s:%d%s"; + if (! $class) { + // Remove method arrow (->) from output. + $template = "\n%3d. %s%s() %s:%d%s"; + } + + $response .= sprintf( + $template, + $line, + $class, + $frame->getFunction(), + $frame->getFile(), + $frame->getLine(), + $this->getFrameArgsOutput($frame, $line) + ); + + $line++; + } + + return $response; + } + + /** + * @return int + */ + public function handle() + { + $response = $this->generateResponse(); + + if ($this->getLogger()) { + $this->getLogger()->error($response); + } + + if (! $this->canOutput()) { + return Handler::DONE; + } + + echo $response; + + return Handler::QUIT; + } + + /** + * @return string + */ + public function contentType() + { + return 'text/plain'; + } +} diff --git a/kirby/vendor/filp/whoops/src/Whoops/Handler/PrettyPageHandler.php b/kirby/vendor/filp/whoops/src/Whoops/Handler/PrettyPageHandler.php new file mode 100755 index 0000000..9f0b655 --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Handler/PrettyPageHandler.php @@ -0,0 +1,713 @@ + + */ + +namespace Whoops\Handler; + +use InvalidArgumentException; +use RuntimeException; +use Symfony\Component\VarDumper\Cloner\AbstractCloner; +use Symfony\Component\VarDumper\Cloner\VarCloner; +use UnexpectedValueException; +use Whoops\Exception\Formatter; +use Whoops\Util\Misc; +use Whoops\Util\TemplateHelper; + +class PrettyPageHandler extends Handler +{ + /** + * Search paths to be scanned for resources, in the reverse + * order they're declared. + * + * @var array + */ + private $searchPaths = []; + + /** + * Fast lookup cache for known resource locations. + * + * @var array + */ + private $resourceCache = []; + + /** + * The name of the custom css file. + * + * @var string + */ + private $customCss = null; + + /** + * @var array[] + */ + private $extraTables = []; + + /** + * @var bool + */ + private $handleUnconditionally = false; + + /** + * @var string + */ + private $pageTitle = "Whoops! There was an error."; + + /** + * @var array[] + */ + private $applicationPaths; + + /** + * @var array[] + */ + private $blacklist = [ + '_GET' => [], + '_POST' => [], + '_FILES' => [], + '_COOKIE' => [], + '_SESSION' => [], + '_SERVER' => [], + '_ENV' => [], + ]; + + /** + * A string identifier for a known IDE/text editor, or a closure + * that resolves a string that can be used to open a given file + * in an editor. If the string contains the special substrings + * %file or %line, they will be replaced with the correct data. + * + * @example + * "txmt://open?url=%file&line=%line" + * @var mixed $editor + */ + protected $editor; + + /** + * A list of known editor strings + * @var array + */ + protected $editors = [ + "sublime" => "subl://open?url=file://%file&line=%line", + "textmate" => "txmt://open?url=file://%file&line=%line", + "emacs" => "emacs://open?url=file://%file&line=%line", + "macvim" => "mvim://open/?url=file://%file&line=%line", + "phpstorm" => "phpstorm://open?file=%file&line=%line", + "idea" => "idea://open?file=%file&line=%line", + "vscode" => "vscode://file/%file:%line", + "atom" => "atom://core/open/file?filename=%file&line=%line", + ]; + + /** + * @var TemplateHelper + */ + private $templateHelper; + + /** + * Constructor. + */ + public function __construct() + { + if (ini_get('xdebug.file_link_format') || extension_loaded('xdebug')) { + // Register editor using xdebug's file_link_format option. + $this->editors['xdebug'] = function ($file, $line) { + return str_replace(['%f', '%l'], [$file, $line], ini_get('xdebug.file_link_format')); + }; + } + + // Add the default, local resource search path: + $this->searchPaths[] = __DIR__ . "/../Resources"; + + // blacklist php provided auth based values + $this->blacklist('_SERVER', 'PHP_AUTH_PW'); + + $this->templateHelper = new TemplateHelper(); + + if (class_exists('Symfony\Component\VarDumper\Cloner\VarCloner')) { + $cloner = new VarCloner(); + // Only dump object internals if a custom caster exists. + $cloner->addCasters(['*' => function ($obj, $a, $stub, $isNested, $filter = 0) { + $class = $stub->class; + $classes = [$class => $class] + class_parents($class) + class_implements($class); + + foreach ($classes as $class) { + if (isset(AbstractCloner::$defaultCasters[$class])) { + return $a; + } + } + + // Remove all internals + return []; + }]); + $this->templateHelper->setCloner($cloner); + } + } + + /** + * @return int|null + */ + public function handle() + { + if (!$this->handleUnconditionally()) { + // Check conditions for outputting HTML: + // @todo: Make this more robust + if (PHP_SAPI === 'cli') { + // Help users who have been relying on an internal test value + // fix their code to the proper method + if (isset($_ENV['whoops-test'])) { + throw new \Exception( + 'Use handleUnconditionally instead of whoops-test' + .' environment variable' + ); + } + + return Handler::DONE; + } + } + + $templateFile = $this->getResource("views/layout.html.php"); + $cssFile = $this->getResource("css/whoops.base.css"); + $zeptoFile = $this->getResource("js/zepto.min.js"); + $prettifyFile = $this->getResource("js/prettify.min.js"); + $clipboard = $this->getResource("js/clipboard.min.js"); + $jsFile = $this->getResource("js/whoops.base.js"); + + if ($this->customCss) { + $customCssFile = $this->getResource($this->customCss); + } + + $inspector = $this->getInspector(); + $frames = $this->getExceptionFrames(); + $code = $this->getExceptionCode(); + + // List of variables that will be passed to the layout template. + $vars = [ + "page_title" => $this->getPageTitle(), + + // @todo: Asset compiler + "stylesheet" => file_get_contents($cssFile), + "zepto" => file_get_contents($zeptoFile), + "prettify" => file_get_contents($prettifyFile), + "clipboard" => file_get_contents($clipboard), + "javascript" => file_get_contents($jsFile), + + // Template paths: + "header" => $this->getResource("views/header.html.php"), + "header_outer" => $this->getResource("views/header_outer.html.php"), + "frame_list" => $this->getResource("views/frame_list.html.php"), + "frames_description" => $this->getResource("views/frames_description.html.php"), + "frames_container" => $this->getResource("views/frames_container.html.php"), + "panel_details" => $this->getResource("views/panel_details.html.php"), + "panel_details_outer" => $this->getResource("views/panel_details_outer.html.php"), + "panel_left" => $this->getResource("views/panel_left.html.php"), + "panel_left_outer" => $this->getResource("views/panel_left_outer.html.php"), + "frame_code" => $this->getResource("views/frame_code.html.php"), + "env_details" => $this->getResource("views/env_details.html.php"), + + "title" => $this->getPageTitle(), + "name" => explode("\\", $inspector->getExceptionName()), + "message" => $inspector->getExceptionMessage(), + "previousMessages" => $inspector->getPreviousExceptionMessages(), + "docref_url" => $inspector->getExceptionDocrefUrl(), + "code" => $code, + "previousCodes" => $inspector->getPreviousExceptionCodes(), + "plain_exception" => Formatter::formatExceptionPlain($inspector), + "frames" => $frames, + "has_frames" => !!count($frames), + "handler" => $this, + "handlers" => $this->getRun()->getHandlers(), + + "active_frames_tab" => count($frames) && $frames->offsetGet(0)->isApplication() ? 'application' : 'all', + "has_frames_tabs" => $this->getApplicationPaths(), + + "tables" => [ + "GET Data" => $this->masked($_GET, '_GET'), + "POST Data" => $this->masked($_POST, '_POST'), + "Files" => isset($_FILES) ? $this->masked($_FILES, '_FILES') : [], + "Cookies" => $this->masked($_COOKIE, '_COOKIE'), + "Session" => isset($_SESSION) ? $this->masked($_SESSION, '_SESSION') : [], + "Server/Request Data" => $this->masked($_SERVER, '_SERVER'), + "Environment Variables" => $this->masked($_ENV, '_ENV'), + ], + ]; + + if (isset($customCssFile)) { + $vars["stylesheet"] .= file_get_contents($customCssFile); + } + + // Add extra entries list of data tables: + // @todo: Consolidate addDataTable and addDataTableCallback + $extraTables = array_map(function ($table) use ($inspector) { + return $table instanceof \Closure ? $table($inspector) : $table; + }, $this->getDataTables()); + $vars["tables"] = array_merge($extraTables, $vars["tables"]); + + $plainTextHandler = new PlainTextHandler(); + $plainTextHandler->setException($this->getException()); + $plainTextHandler->setInspector($this->getInspector()); + $vars["preface"] = ""; + + $this->templateHelper->setVariables($vars); + $this->templateHelper->render($templateFile); + + return Handler::QUIT; + } + + /** + * Get the stack trace frames of the exception that is currently being handled. + * + * @return \Whoops\Exception\FrameCollection; + */ + protected function getExceptionFrames() + { + $frames = $this->getInspector()->getFrames(); + + if ($this->getApplicationPaths()) { + foreach ($frames as $frame) { + foreach ($this->getApplicationPaths() as $path) { + if (strpos($frame->getFile(), $path) === 0) { + $frame->setApplication(true); + break; + } + } + } + } + + return $frames; + } + + /** + * Get the code of the exception that is currently being handled. + * + * @return string + */ + protected function getExceptionCode() + { + $exception = $this->getException(); + + $code = $exception->getCode(); + if ($exception instanceof \ErrorException) { + // ErrorExceptions wrap the php-error types within the 'severity' property + $code = Misc::translateErrorCode($exception->getSeverity()); + } + + return (string) $code; + } + + /** + * @return string + */ + public function contentType() + { + return 'text/html'; + } + + /** + * Adds an entry to the list of tables displayed in the template. + * The expected data is a simple associative array. Any nested arrays + * will be flattened with print_r + * @param string $label + * @param array $data + */ + public function addDataTable($label, array $data) + { + $this->extraTables[$label] = $data; + } + + /** + * Lazily adds an entry to the list of tables displayed in the table. + * The supplied callback argument will be called when the error is rendered, + * it should produce a simple associative array. Any nested arrays will + * be flattened with print_r. + * + * @throws InvalidArgumentException If $callback is not callable + * @param string $label + * @param callable $callback Callable returning an associative array + */ + public function addDataTableCallback($label, /* callable */ $callback) + { + if (!is_callable($callback)) { + throw new InvalidArgumentException('Expecting callback argument to be callable'); + } + + $this->extraTables[$label] = function (\Whoops\Exception\Inspector $inspector = null) use ($callback) { + try { + $result = call_user_func($callback, $inspector); + + // Only return the result if it can be iterated over by foreach(). + return is_array($result) || $result instanceof \Traversable ? $result : []; + } catch (\Exception $e) { + // Don't allow failure to break the rendering of the original exception. + return []; + } + }; + } + + /** + * Returns all the extra data tables registered with this handler. + * Optionally accepts a 'label' parameter, to only return the data + * table under that label. + * @param string|null $label + * @return array[]|callable + */ + public function getDataTables($label = null) + { + if ($label !== null) { + return isset($this->extraTables[$label]) ? + $this->extraTables[$label] : []; + } + + return $this->extraTables; + } + + /** + * Allows to disable all attempts to dynamically decide whether to + * handle or return prematurely. + * Set this to ensure that the handler will perform no matter what. + * @param bool|null $value + * @return bool|null + */ + public function handleUnconditionally($value = null) + { + if (func_num_args() == 0) { + return $this->handleUnconditionally; + } + + $this->handleUnconditionally = (bool) $value; + } + + /** + * Adds an editor resolver, identified by a string + * name, and that may be a string path, or a callable + * resolver. If the callable returns a string, it will + * be set as the file reference's href attribute. + * + * @example + * $run->addEditor('macvim', "mvim://open?url=file://%file&line=%line") + * @example + * $run->addEditor('remove-it', function($file, $line) { + * unlink($file); + * return "http://stackoverflow.com"; + * }); + * @param string $identifier + * @param string $resolver + */ + public function addEditor($identifier, $resolver) + { + $this->editors[$identifier] = $resolver; + } + + /** + * Set the editor to use to open referenced files, by a string + * identifier, or a callable that will be executed for every + * file reference, with a $file and $line argument, and should + * return a string. + * + * @example + * $run->setEditor(function($file, $line) { return "file:///{$file}"; }); + * @example + * $run->setEditor('sublime'); + * + * @throws InvalidArgumentException If invalid argument identifier provided + * @param string|callable $editor + */ + public function setEditor($editor) + { + if (!is_callable($editor) && !isset($this->editors[$editor])) { + throw new InvalidArgumentException( + "Unknown editor identifier: $editor. Known editors:" . + implode(",", array_keys($this->editors)) + ); + } + + $this->editor = $editor; + } + + /** + * Given a string file path, and an integer file line, + * executes the editor resolver and returns, if available, + * a string that may be used as the href property for that + * file reference. + * + * @throws InvalidArgumentException If editor resolver does not return a string + * @param string $filePath + * @param int $line + * @return string|bool + */ + public function getEditorHref($filePath, $line) + { + $editor = $this->getEditor($filePath, $line); + + if (empty($editor)) { + return false; + } + + // Check that the editor is a string, and replace the + // %line and %file placeholders: + if (!isset($editor['url']) || !is_string($editor['url'])) { + throw new UnexpectedValueException( + __METHOD__ . " should always resolve to a string or a valid editor array; got something else instead." + ); + } + + $editor['url'] = str_replace("%line", rawurlencode($line), $editor['url']); + $editor['url'] = str_replace("%file", rawurlencode($filePath), $editor['url']); + + return $editor['url']; + } + + /** + * Given a boolean if the editor link should + * act as an Ajax request. The editor must be a + * valid callable function/closure + * + * @throws UnexpectedValueException If editor resolver does not return a boolean + * @param string $filePath + * @param int $line + * @return bool + */ + public function getEditorAjax($filePath, $line) + { + $editor = $this->getEditor($filePath, $line); + + // Check that the ajax is a bool + if (!isset($editor['ajax']) || !is_bool($editor['ajax'])) { + throw new UnexpectedValueException( + __METHOD__ . " should always resolve to a bool; got something else instead." + ); + } + return $editor['ajax']; + } + + /** + * Given a boolean if the editor link should + * act as an Ajax request. The editor must be a + * valid callable function/closure + * + * @param string $filePath + * @param int $line + * @return array + */ + protected function getEditor($filePath, $line) + { + if (!$this->editor || (!is_string($this->editor) && !is_callable($this->editor))) { + return []; + } + + if (is_string($this->editor) && isset($this->editors[$this->editor]) && !is_callable($this->editors[$this->editor])) { + return [ + 'ajax' => false, + 'url' => $this->editors[$this->editor], + ]; + } + + if (is_callable($this->editor) || (isset($this->editors[$this->editor]) && is_callable($this->editors[$this->editor]))) { + if (is_callable($this->editor)) { + $callback = call_user_func($this->editor, $filePath, $line); + } else { + $callback = call_user_func($this->editors[$this->editor], $filePath, $line); + } + + if (empty($callback)) { + return []; + } + + if (is_string($callback)) { + return [ + 'ajax' => false, + 'url' => $callback, + ]; + } + + return [ + 'ajax' => isset($callback['ajax']) ? $callback['ajax'] : false, + 'url' => isset($callback['url']) ? $callback['url'] : $callback, + ]; + } + + return []; + } + + /** + * @param string $title + * @return void + */ + public function setPageTitle($title) + { + $this->pageTitle = (string) $title; + } + + /** + * @return string + */ + public function getPageTitle() + { + return $this->pageTitle; + } + + /** + * Adds a path to the list of paths to be searched for + * resources. + * + * @throws InvalidArgumentException If $path is not a valid directory + * + * @param string $path + * @return void + */ + public function addResourcePath($path) + { + if (!is_dir($path)) { + throw new InvalidArgumentException( + "'$path' is not a valid directory" + ); + } + + array_unshift($this->searchPaths, $path); + } + + /** + * Adds a custom css file to be loaded. + * + * @param string $name + * @return void + */ + public function addCustomCss($name) + { + $this->customCss = $name; + } + + /** + * @return array + */ + public function getResourcePaths() + { + return $this->searchPaths; + } + + /** + * Finds a resource, by its relative path, in all available search paths. + * The search is performed starting at the last search path, and all the + * way back to the first, enabling a cascading-type system of overrides + * for all resources. + * + * @throws RuntimeException If resource cannot be found in any of the available paths + * + * @param string $resource + * @return string + */ + protected function getResource($resource) + { + // If the resource was found before, we can speed things up + // by caching its absolute, resolved path: + if (isset($this->resourceCache[$resource])) { + return $this->resourceCache[$resource]; + } + + // Search through available search paths, until we find the + // resource we're after: + foreach ($this->searchPaths as $path) { + $fullPath = $path . "/$resource"; + + if (is_file($fullPath)) { + // Cache the result: + $this->resourceCache[$resource] = $fullPath; + return $fullPath; + } + } + + // If we got this far, nothing was found. + throw new RuntimeException( + "Could not find resource '$resource' in any resource paths." + . "(searched: " . join(", ", $this->searchPaths). ")" + ); + } + + /** + * @deprecated + * + * @return string + */ + public function getResourcesPath() + { + $allPaths = $this->getResourcePaths(); + + // Compat: return only the first path added + return end($allPaths) ?: null; + } + + /** + * @deprecated + * + * @param string $resourcesPath + * @return void + */ + public function setResourcesPath($resourcesPath) + { + $this->addResourcePath($resourcesPath); + } + + /** + * Return the application paths. + * + * @return array + */ + public function getApplicationPaths() + { + return $this->applicationPaths; + } + + /** + * Set the application paths. + * + * @param array $applicationPaths + */ + public function setApplicationPaths($applicationPaths) + { + $this->applicationPaths = $applicationPaths; + } + + /** + * Set the application root path. + * + * @param string $applicationRootPath + */ + public function setApplicationRootPath($applicationRootPath) + { + $this->templateHelper->setApplicationRootPath($applicationRootPath); + } + + /** + * blacklist a sensitive value within one of the superglobal arrays. + * + * @param $superGlobalName string the name of the superglobal array, e.g. '_GET' + * @param $key string the key within the superglobal + */ + public function blacklist($superGlobalName, $key) + { + $this->blacklist[$superGlobalName][] = $key; + } + + /** + * Checks all values within the given superGlobal array. + * Blacklisted values will be replaced by a equal length string cointaining only '*' characters. + * + * We intentionally dont rely on $GLOBALS as it depends on 'auto_globals_jit' php.ini setting. + * + * @param $superGlobal array One of the superglobal arrays + * @param $superGlobalName string the name of the superglobal array, e.g. '_GET' + * @return array $values without sensitive data + */ + private function masked(array $superGlobal, $superGlobalName) + { + $blacklisted = $this->blacklist[$superGlobalName]; + + $values = $superGlobal; + foreach ($blacklisted as $key) { + if (isset($superGlobal[$key])) { + $values[$key] = str_repeat('*', strlen($superGlobal[$key])); + } + } + return $values; + } +} diff --git a/kirby/vendor/filp/whoops/src/Whoops/Handler/XmlResponseHandler.php b/kirby/vendor/filp/whoops/src/Whoops/Handler/XmlResponseHandler.php new file mode 100755 index 0000000..0d0a577 --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Handler/XmlResponseHandler.php @@ -0,0 +1,107 @@ + + */ + +namespace Whoops\Handler; + +use SimpleXMLElement; +use Whoops\Exception\Formatter; + +/** + * Catches an exception and converts it to an XML + * response. Additionally can also return exception + * frames for consumption by an API. + */ +class XmlResponseHandler extends Handler +{ + /** + * @var bool + */ + private $returnFrames = false; + + /** + * @param bool|null $returnFrames + * @return bool|$this + */ + public function addTraceToOutput($returnFrames = null) + { + if (func_num_args() == 0) { + return $this->returnFrames; + } + + $this->returnFrames = (bool) $returnFrames; + return $this; + } + + /** + * @return int + */ + public function handle() + { + $response = [ + 'error' => Formatter::formatExceptionAsDataArray( + $this->getInspector(), + $this->addTraceToOutput() + ), + ]; + + echo $this->toXml($response); + + return Handler::QUIT; + } + + /** + * @return string + */ + public function contentType() + { + return 'application/xml'; + } + + /** + * @param SimpleXMLElement $node Node to append data to, will be modified in place + * @param array|\Traversable $data + * @return SimpleXMLElement The modified node, for chaining + */ + private static function addDataToNode(\SimpleXMLElement $node, $data) + { + assert(is_array($data) || $data instanceof Traversable); + + foreach ($data as $key => $value) { + if (is_numeric($key)) { + // Convert the key to a valid string + $key = "unknownNode_". (string) $key; + } + + // Delete any char not allowed in XML element names + $key = preg_replace('/[^a-z0-9\-\_\.\:]/i', '', $key); + + if (is_array($value)) { + $child = $node->addChild($key); + self::addDataToNode($child, $value); + } else { + $value = str_replace('&', '&', print_r($value, true)); + $node->addChild($key, $value); + } + } + + return $node; + } + + /** + * The main function for converting to an XML document. + * + * @param array|\Traversable $data + * @return string XML + */ + private static function toXml($data) + { + assert(is_array($data) || $data instanceof Traversable); + + $node = simplexml_load_string(""); + + return self::addDataToNode($node, $data)->asXML(); + } +} diff --git a/kirby/vendor/filp/whoops/src/Whoops/Resources/css/whoops.base.css b/kirby/vendor/filp/whoops/src/Whoops/Resources/css/whoops.base.css new file mode 100755 index 0000000..1e3d77e --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Resources/css/whoops.base.css @@ -0,0 +1,653 @@ +body { + font: 12px "Helvetica Neue", helvetica, arial, sans-serif; + color: #131313; + background: #eeeeee; + padding: 0; + margin: 0; + max-height: 100%; + + text-rendering: optimizeLegibility; +} +a { + text-decoration: none; +} + +.panel { + overflow-y: scroll; + height: 100%; + position: fixed; + margin: 0; + left: 0; + top: 0; +} + +.branding { + position: absolute; + top: 10px; + right: 20px; + color: #777777; + font-size: 10px; + z-index: 100; +} +.branding a { + color: #e95353; +} + +header { + color: white; + box-sizing: border-box; + background-color: #2a2a2a; + padding: 35px 40px; + max-height: 180px; + overflow: hidden; + transition: 0.5s; +} + +header.header-expand { + max-height: 1000px; +} + +.exc-title { + margin: 0; + color: #bebebe; + font-size: 14px; +} +.exc-title-primary, +.exc-title-secondary { + color: #e95353; +} + +.exc-message { + font-size: 20px; + word-wrap: break-word; + margin: 4px 0 0 0; + color: white; +} +.exc-message span { + display: block; +} +.exc-message-empty-notice { + color: #a29d9d; + font-weight: 300; +} + +.prev-exc-title { + margin: 10px 0; +} + +.prev-exc-title + ul { + margin: 0; + padding: 0 0 0 20px; + line-height: 12px; +} + +.prev-exc-title + ul li { + font: 12px "Helvetica Neue", helvetica, arial, sans-serif; +} + +.prev-exc-title + ul li .prev-exc-code { + display: inline-block; + color: #bebebe; +} + +.details-container { + left: 30%; + width: 70%; + background: #fafafa; +} +.details { + padding: 5px; +} + +.details-heading { + color: #4288ce; + font-weight: 300; + padding-bottom: 10px; + margin-bottom: 10px; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); +} + +.details pre.sf-dump { + white-space: pre; + word-wrap: inherit; +} + +.details pre.sf-dump, +.details pre.sf-dump .sf-dump-num, +.details pre.sf-dump .sf-dump-const, +.details pre.sf-dump .sf-dump-str, +.details pre.sf-dump .sf-dump-note, +.details pre.sf-dump .sf-dump-ref, +.details pre.sf-dump .sf-dump-public, +.details pre.sf-dump .sf-dump-protected, +.details pre.sf-dump .sf-dump-private, +.details pre.sf-dump .sf-dump-meta, +.details pre.sf-dump .sf-dump-key, +.details pre.sf-dump .sf-dump-index { + color: #463c54; +} + +.left-panel { + width: 30%; + background: #ded8d8; +} + +.frames-description { + background: rgba(0, 0, 0, 0.05); + padding: 8px 15px; + color: #a29d9d; + font-size: 11px; +} + +.frames-description.frames-description-application { + text-align: center; + font-size: 12px; +} +.frames-container.frames-container-application .frame:not(.frame-application) { + display: none; +} + +.frames-tab { + color: #a29d9d; + display: inline-block; + padding: 4px 8px; + margin: 0 2px; + border-radius: 3px; +} + +.frames-tab.frames-tab-active { + background-color: #2a2a2a; + color: #bebebe; +} + +.frame { + padding: 14px; + cursor: pointer; + transition: all 0.1s ease; + background: #eeeeee; +} +.frame:not(:last-child) { + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.frame.active { + box-shadow: inset -5px 0 0 0 #4288ce; + color: #4288ce; +} + +.frame:not(.active):hover { + background: #bee9ea; +} + +.frame-method-info { + margin-bottom: 10px; +} + +.frame-class, +.frame-function, +.frame-index { + font-size: 14px; +} + +.frame-index { + float: left; +} + +.frame-method-info { + margin-left: 24px; +} + +.frame-index { + font-size: 11px; + color: #a29d9d; + background-color: rgba(0, 0, 0, 0.05); + height: 18px; + width: 18px; + line-height: 18px; + border-radius: 5px; + padding: 0 1px 0 1px; + text-align: center; + display: inline-block; +} + +.frame-application .frame-index { + background-color: #2a2a2a; + color: #bebebe; +} + +.frame-file { + font-family: "Inconsolata", "Fira Mono", "Source Code Pro", Monaco, Consolas, + "Lucida Console", monospace; + color: #a29d9d; +} + +.frame-file .editor-link { + color: #a29d9d; +} + +.frame-line { + font-weight: bold; +} + +.frame-line:before { + content: ":"; +} + +.frame-code { + padding: 5px; + background: #303030; + display: none; +} + +.frame-code.active { + display: block; +} + +.frame-code .frame-file { + color: #a29d9d; + padding: 12px 6px; + + border-bottom: none; +} + +.code-block { + padding: 10px; + margin: 0; + border-radius: 6px; + box-shadow: 0 3px 0 rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.05), + inset 0 0 1px 0 rgba(255, 255, 255, 0.07); + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; +} + +.linenums { + margin: 0; + margin-left: 10px; +} + +.frame-comments { + border-top: none; + margin-top: 15px; + + font-size: 12px; +} + +.frame-comments.empty { +} + +.frame-comments.empty:before { + content: "No comments for this stack frame."; + font-weight: 300; + color: #a29d9d; +} + +.frame-comment { + padding: 10px; + color: #e3e3e3; + border-radius: 6px; + background-color: rgba(255, 255, 255, 0.05); +} +.frame-comment a { + font-weight: bold; + text-decoration: none; +} +.frame-comment a:hover { + color: #4bb1b1; +} + +.frame-comment:not(:last-child) { + border-bottom: 1px dotted rgba(0, 0, 0, 0.3); +} + +.frame-comment-context { + font-size: 10px; + color: white; +} + +.delimiter { + display: inline-block; +} + +.data-table-container label { + font-size: 16px; + color: #303030; + font-weight: bold; + margin: 10px 0; + + display: block; + + margin-bottom: 5px; + padding-bottom: 5px; +} +.data-table { + width: 100%; + margin-bottom: 10px; +} + +.data-table tbody { + font: 13px "Inconsolata", "Fira Mono", "Source Code Pro", Monaco, Consolas, + "Lucida Console", monospace; +} + +.data-table thead { + display: none; +} + +.data-table tr { + padding: 5px 0; +} + +.data-table td:first-child { + width: 20%; + min-width: 130px; + overflow: hidden; + font-weight: bold; + color: #463c54; + padding-right: 5px; +} + +.data-table td:last-child { + width: 80%; + -ms-word-break: break-all; + word-break: break-all; + word-break: break-word; + -webkit-hyphens: auto; + -moz-hyphens: auto; + hyphens: auto; +} + +.data-table span.empty { + color: rgba(0, 0, 0, 0.3); + font-weight: 300; +} +.data-table label.empty { + display: inline; +} + +.handler { + padding: 4px 0; + font: 14px "Inconsolata", "Fira Mono", "Source Code Pro", Monaco, Consolas, + "Lucida Console", monospace; +} + +/* prettify code style +Uses the Doxy theme as a base */ +pre .str, +code .str { + color: #bcd42a; +} /* string */ +pre .kwd, +code .kwd { + color: #4bb1b1; + font-weight: bold; +} /* keyword*/ +pre .com, +code .com { + color: #888; + font-weight: bold; +} /* comment */ +pre .typ, +code .typ { + color: #ef7c61; +} /* type */ +pre .lit, +code .lit { + color: #bcd42a; +} /* literal */ +pre .pun, +code .pun { + color: #fff; + font-weight: bold; +} /* punctuation */ +pre .pln, +code .pln { + color: #e9e4e5; +} /* plaintext */ +pre .tag, +code .tag { + color: #4bb1b1; +} /* html/xml tag */ +pre .htm, +code .htm { + color: #dda0dd; +} /* html tag */ +pre .xsl, +code .xsl { + color: #d0a0d0; +} /* xslt tag */ +pre .atn, +code .atn { + color: #ef7c61; + font-weight: normal; +} /* html/xml attribute name */ +pre .atv, +code .atv { + color: #bcd42a; +} /* html/xml attribute value */ +pre .dec, +code .dec { + color: #606; +} /* decimal */ +pre.code-block, +code.code-block, +.frame-args.code-block, +.frame-args.code-block samp { + font-family: "Inconsolata", "Fira Mono", "Source Code Pro", Monaco, Consolas, + "Lucida Console", monospace; + background: #333; + color: #e9e4e5; +} +pre.code-block { + white-space: pre-wrap; +} + +pre.code-block a, +code.code-block a { + text-decoration: none; +} + +.linenums li { + color: #a5a5a5; +} + +.linenums li.current { + background: rgba(255, 100, 100, 0.07); +} +.linenums li.current.active { + background: rgba(255, 100, 100, 0.17); +} + +pre:not(.prettyprinted) { + padding-left: 60px; +} + +#plain-exception { + display: none; +} + +#copy-button { + cursor: pointer; + border: 0; +} + +.clipboard { + opacity: 0.8; + background: none; + + color: rgba(255, 255, 255, 0.1); + box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.1); + + border-radius: 3px; + + outline: none !important; +} + +.clipboard:hover { + box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.3); + color: rgba(255, 255, 255, 0.3); +} + +/* inspired by githubs kbd styles */ +kbd { + -moz-border-bottom-colors: none; + -moz-border-left-colors: none; + -moz-border-right-colors: none; + -moz-border-top-colors: none; + background-color: #fcfcfc; + border-color: #ccc #ccc #bbb; + border-image: none; + border-style: solid; + border-width: 1px; + color: #555; + display: inline-block; + font-size: 11px; + line-height: 10px; + padding: 3px 5px; + vertical-align: middle; +} + +/* == Media queries */ + +/* Expand the spacing in the details section */ +@media (min-width: 1000px) { + .details, + .frame-code { + padding: 20px 40px; + } + + .details-container { + left: 32%; + width: 68%; + } + + .frames-container { + margin: 5px; + } + + .left-panel { + width: 32%; + } +} + +/* Stack panels */ +@media (max-width: 600px) { + .panel { + position: static; + width: 100%; + } +} + +/* Stack details tables */ +@media (max-width: 400px) { + .data-table, + .data-table tbody, + .data-table tbody tr, + .data-table tbody td { + display: block; + width: 100%; + } + + .data-table tbody tr:first-child { + padding-top: 0; + } + + .data-table tbody td:first-child, + .data-table tbody td:last-child { + padding-left: 0; + padding-right: 0; + } + + .data-table tbody td:last-child { + padding-top: 3px; + } +} + +.tooltipped { + position: relative; +} +.tooltipped:after { + position: absolute; + z-index: 1000000; + display: none; + padding: 5px 8px; + color: #fff; + text-align: center; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-wrap: break-word; + white-space: pre; + pointer-events: none; + content: attr(aria-label); + background: rgba(0, 0, 0, 0.8); + border-radius: 3px; + -webkit-font-smoothing: subpixel-antialiased; +} +.tooltipped:before { + position: absolute; + z-index: 1000001; + display: none; + width: 0; + height: 0; + color: rgba(0, 0, 0, 0.8); + pointer-events: none; + content: ""; + border: 5px solid transparent; +} +.tooltipped:hover:before, +.tooltipped:hover:after, +.tooltipped:active:before, +.tooltipped:active:after, +.tooltipped:focus:before, +.tooltipped:focus:after { + display: inline-block; + text-decoration: none; +} +.tooltipped-s:after { + top: 100%; + right: 50%; + margin-top: 5px; +} +.tooltipped-s:before { + top: auto; + right: 50%; + bottom: -5px; + margin-right: -5px; + border-bottom-color: rgba(0, 0, 0, 0.8); +} + +pre.sf-dump { + padding: 0px !important; + margin: 0px !important; +} + +.search-for-help { + width: 85%; + padding: 0; + margin: 10px 0; + list-style-type: none; + display: inline-block; +} +.search-for-help li { + display: inline-block; + margin-right: 5px; +} +.search-for-help li:last-child { + margin-right: 0; +} +.search-for-help li a { +} +.search-for-help li a i { + width: 16px; + height: 16px; + overflow: hidden; + display: block; +} +.search-for-help li a svg { + fill: #fff; +} +.search-for-help li a svg path { + background-size: contain; +} diff --git a/kirby/vendor/filp/whoops/src/Whoops/Resources/js/clipboard.min.js b/kirby/vendor/filp/whoops/src/Whoops/Resources/js/clipboard.min.js new file mode 100755 index 0000000..aeeb51d --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Resources/js/clipboard.min.js @@ -0,0 +1,523 @@ +/*! + * clipboard.js v1.5.3 + * https://zenorocha.github.io/clipboard.js + * + * Licensed MIT © Zeno Rocha + */ +!(function(t) { + if ("object" == typeof exports && "undefined" != typeof module) + module.exports = t(); + else if ("function" == typeof define && define.amd) define([], t); + else { + var e; + (e = + "undefined" != typeof window + ? window + : "undefined" != typeof global + ? global + : "undefined" != typeof self + ? self + : this), + (e.Clipboard = t()); + } +})(function() { + var t, e, n; + return (function t(e, n, r) { + function o(a, c) { + if (!n[a]) { + if (!e[a]) { + var s = "function" == typeof require && require; + if (!c && s) return s(a, !0); + if (i) return i(a, !0); + var u = new Error("Cannot find module '" + a + "'"); + throw ((u.code = "MODULE_NOT_FOUND"), u); + } + var l = (n[a] = { exports: {} }); + e[a][0].call( + l.exports, + function(t) { + var n = e[a][1][t]; + return o(n ? n : t); + }, + l, + l.exports, + t, + e, + n, + r + ); + } + return n[a].exports; + } + for ( + var i = "function" == typeof require && require, a = 0; + a < r.length; + a++ + ) + o(r[a]); + return o; + })( + { + 1: [ + function(t, e, n) { + var r = t("matches-selector"); + e.exports = function(t, e, n) { + for (var o = n ? t : t.parentNode; o && o !== document; ) { + if (r(o, e)) return o; + o = o.parentNode; + } + }; + }, + { "matches-selector": 2 } + ], + 2: [ + function(t, e, n) { + function r(t, e) { + if (i) return i.call(t, e); + for ( + var n = t.parentNode.querySelectorAll(e), r = 0; + r < n.length; + ++r + ) + if (n[r] == t) return !0; + return !1; + } + var o = Element.prototype, + i = + o.matchesSelector || + o.webkitMatchesSelector || + o.mozMatchesSelector || + o.msMatchesSelector || + o.oMatchesSelector; + e.exports = r; + }, + {} + ], + 3: [ + function(t, e, n) { + function r(t, e, n, r) { + var i = o.apply(this, arguments); + return ( + t.addEventListener(n, i), + { + destroy: function() { + t.removeEventListener(n, i); + } + } + ); + } + function o(t, e, n, r) { + return function(n) { + var o = i(n.target, e, !0); + o && + (Object.defineProperty(n, "target", { value: o }), + r.call(t, n)); + }; + } + var i = t("closest"); + e.exports = r; + }, + { closest: 1 } + ], + 4: [ + function(t, e, n) { + (n.node = function(t) { + return void 0 !== t && t instanceof HTMLElement && 1 === t.nodeType; + }), + (n.nodeList = function(t) { + var e = Object.prototype.toString.call(t); + return ( + void 0 !== t && + ("[object NodeList]" === e || + "[object HTMLCollection]" === e) && + "length" in t && + (0 === t.length || n.node(t[0])) + ); + }), + (n.string = function(t) { + return "string" == typeof t || t instanceof String; + }), + (n.function = function(t) { + var e = Object.prototype.toString.call(t); + return "[object Function]" === e; + }); + }, + {} + ], + 5: [ + function(t, e, n) { + function r(t, e, n) { + if (!t && !e && !n) throw new Error("Missing required arguments"); + if (!c.string(e)) + throw new TypeError("Second argument must be a String"); + if (!c.function(n)) + throw new TypeError("Third argument must be a Function"); + if (c.node(t)) return o(t, e, n); + if (c.nodeList(t)) return i(t, e, n); + if (c.string(t)) return a(t, e, n); + throw new TypeError( + "First argument must be a String, HTMLElement, HTMLCollection, or NodeList" + ); + } + function o(t, e, n) { + return ( + t.addEventListener(e, n), + { + destroy: function() { + t.removeEventListener(e, n); + } + } + ); + } + function i(t, e, n) { + return ( + Array.prototype.forEach.call(t, function(t) { + t.addEventListener(e, n); + }), + { + destroy: function() { + Array.prototype.forEach.call(t, function(t) { + t.removeEventListener(e, n); + }); + } + } + ); + } + function a(t, e, n) { + return s(document.body, t, e, n); + } + var c = t("./is"), + s = t("delegate"); + e.exports = r; + }, + { "./is": 4, delegate: 3 } + ], + 6: [ + function(t, e, n) { + function r(t) { + var e; + if ("INPUT" === t.nodeName || "TEXTAREA" === t.nodeName) + t.select(), (e = t.value); + else { + var n = window.getSelection(), + r = document.createRange(); + r.selectNodeContents(t), + n.removeAllRanges(), + n.addRange(r), + (e = n.toString()); + } + return e; + } + e.exports = r; + }, + {} + ], + 7: [ + function(t, e, n) { + function r() {} + (r.prototype = { + on: function(t, e, n) { + var r = this.e || (this.e = {}); + return (r[t] || (r[t] = [])).push({ fn: e, ctx: n }), this; + }, + once: function(t, e, n) { + function r() { + o.off(t, r), e.apply(n, arguments); + } + var o = this; + return (r._ = e), this.on(t, r, n); + }, + emit: function(t) { + var e = [].slice.call(arguments, 1), + n = ((this.e || (this.e = {}))[t] || []).slice(), + r = 0, + o = n.length; + for (r; o > r; r++) n[r].fn.apply(n[r].ctx, e); + return this; + }, + off: function(t, e) { + var n = this.e || (this.e = {}), + r = n[t], + o = []; + if (r && e) + for (var i = 0, a = r.length; a > i; i++) + r[i].fn !== e && r[i].fn._ !== e && o.push(r[i]); + return o.length ? (n[t] = o) : delete n[t], this; + } + }), + (e.exports = r); + }, + {} + ], + 8: [ + function(t, e, n) { + "use strict"; + function r(t) { + return t && t.__esModule ? t : { default: t }; + } + function o(t, e) { + if (!(t instanceof e)) + throw new TypeError("Cannot call a class as a function"); + } + n.__esModule = !0; + var i = (function() { + function t(t, e) { + for (var n = 0; n < e.length; n++) { + var r = e[n]; + (r.enumerable = r.enumerable || !1), + (r.configurable = !0), + "value" in r && (r.writable = !0), + Object.defineProperty(t, r.key, r); + } + } + return function(e, n, r) { + return n && t(e.prototype, n), r && t(e, r), e; + }; + })(), + a = t("select"), + c = r(a), + s = (function() { + function t(e) { + o(this, t), this.resolveOptions(e), this.initSelection(); + } + return ( + (t.prototype.resolveOptions = function t() { + var e = + arguments.length <= 0 || void 0 === arguments[0] + ? {} + : arguments[0]; + (this.action = e.action), + (this.emitter = e.emitter), + (this.target = e.target), + (this.text = e.text), + (this.trigger = e.trigger), + (this.selectedText = ""); + }), + (t.prototype.initSelection = function t() { + if (this.text && this.target) + throw new Error( + 'Multiple attributes declared, use either "target" or "text"' + ); + if (this.text) this.selectFake(); + else { + if (!this.target) + throw new Error( + 'Missing required attributes, use either "target" or "text"' + ); + this.selectTarget(); + } + }), + (t.prototype.selectFake = function t() { + var e = this; + this.removeFake(), + (this.fakeHandler = document.body.addEventListener( + "click", + function() { + return e.removeFake(); + } + )), + (this.fakeElem = document.createElement("textarea")), + (this.fakeElem.style.position = "absolute"), + (this.fakeElem.style.left = "-9999px"), + (this.fakeElem.style.top = + (window.pageYOffset || + document.documentElement.scrollTop) + "px"), + this.fakeElem.setAttribute("readonly", ""), + (this.fakeElem.value = this.text), + document.body.appendChild(this.fakeElem), + (this.selectedText = c.default(this.fakeElem)), + this.copyText(); + }), + (t.prototype.removeFake = function t() { + this.fakeHandler && + (document.body.removeEventListener("click"), + (this.fakeHandler = null)), + this.fakeElem && + (document.body.removeChild(this.fakeElem), + (this.fakeElem = null)); + }), + (t.prototype.selectTarget = function t() { + (this.selectedText = c.default(this.target)), this.copyText(); + }), + (t.prototype.copyText = function t() { + var e = void 0; + try { + e = document.execCommand(this.action); + } catch (n) { + e = !1; + } + this.handleResult(e); + }), + (t.prototype.handleResult = function t(e) { + e + ? this.emitter.emit("success", { + action: this.action, + text: this.selectedText, + trigger: this.trigger, + clearSelection: this.clearSelection.bind(this) + }) + : this.emitter.emit("error", { + action: this.action, + trigger: this.trigger, + clearSelection: this.clearSelection.bind(this) + }); + }), + (t.prototype.clearSelection = function t() { + this.target && this.target.blur(), + window.getSelection().removeAllRanges(); + }), + (t.prototype.destroy = function t() { + this.removeFake(); + }), + i(t, [ + { + key: "action", + set: function t() { + var e = + arguments.length <= 0 || void 0 === arguments[0] + ? "copy" + : arguments[0]; + if ( + ((this._action = e), + "copy" !== this._action && "cut" !== this._action) + ) + throw new Error( + 'Invalid "action" value, use either "copy" or "cut"' + ); + }, + get: function t() { + return this._action; + } + }, + { + key: "target", + set: function t(e) { + if (void 0 !== e) { + if (!e || "object" != typeof e || 1 !== e.nodeType) + throw new Error( + 'Invalid "target" value, use a valid Element' + ); + this._target = e; + } + }, + get: function t() { + return this._target; + } + } + ]), + t + ); + })(); + (n.default = s), (e.exports = n.default); + }, + { select: 6 } + ], + 9: [ + function(t, e, n) { + "use strict"; + function r(t) { + return t && t.__esModule ? t : { default: t }; + } + function o(t, e) { + if (!(t instanceof e)) + throw new TypeError("Cannot call a class as a function"); + } + function i(t, e) { + if ("function" != typeof e && null !== e) + throw new TypeError( + "Super expression must either be null or a function, not " + + typeof e + ); + (t.prototype = Object.create(e && e.prototype, { + constructor: { + value: t, + enumerable: !1, + writable: !0, + configurable: !0 + } + })), + e && + (Object.setPrototypeOf + ? Object.setPrototypeOf(t, e) + : (t.__proto__ = e)); + } + function a(t, e) { + var n = "data-clipboard-" + t; + if (e.hasAttribute(n)) return e.getAttribute(n); + } + n.__esModule = !0; + var c = t("./clipboard-action"), + s = r(c), + u = t("tiny-emitter"), + l = r(u), + f = t("good-listener"), + d = r(f), + h = (function(t) { + function e(n, r) { + o(this, e), + t.call(this), + this.resolveOptions(r), + this.listenClick(n); + } + return ( + i(e, t), + (e.prototype.resolveOptions = function t() { + var e = + arguments.length <= 0 || void 0 === arguments[0] + ? {} + : arguments[0]; + (this.action = + "function" == typeof e.action + ? e.action + : this.defaultAction), + (this.target = + "function" == typeof e.target + ? e.target + : this.defaultTarget), + (this.text = + "function" == typeof e.text ? e.text : this.defaultText); + }), + (e.prototype.listenClick = function t(e) { + var n = this; + this.listener = d.default(e, "click", function(t) { + return n.onClick(t); + }); + }), + (e.prototype.onClick = function t(e) { + this.clipboardAction && (this.clipboardAction = null), + (this.clipboardAction = new s.default({ + action: this.action(e.target), + target: this.target(e.target), + text: this.text(e.target), + trigger: e.target, + emitter: this + })); + }), + (e.prototype.defaultAction = function t(e) { + return a("action", e); + }), + (e.prototype.defaultTarget = function t(e) { + var n = a("target", e); + return n ? document.querySelector(n) : void 0; + }), + (e.prototype.defaultText = function t(e) { + return a("text", e); + }), + (e.prototype.destroy = function t() { + this.listener.destroy(), + this.clipboardAction && + (this.clipboardAction.destroy(), + (this.clipboardAction = null)); + }), + e + ); + })(l.default); + (n.default = h), (e.exports = n.default); + }, + { "./clipboard-action": 8, "good-listener": 5, "tiny-emitter": 7 } + ] + }, + {}, + [9] + )(9); +}); diff --git a/kirby/vendor/filp/whoops/src/Whoops/Resources/js/prettify.min.js b/kirby/vendor/filp/whoops/src/Whoops/Resources/js/prettify.min.js new file mode 100755 index 0000000..1d03798 --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Resources/js/prettify.min.js @@ -0,0 +1,753 @@ +var r = null; +window.PR_SHOULD_USE_CONTINUATION = !0; +(function() { + function O(a) { + function i(d) { + var a = d.charCodeAt(0); + if (a !== 92) return a; + var f = d.charAt(1); + return (a = s[f]) + ? a + : "0" <= f && f <= "7" + ? parseInt(d.substring(1), 8) + : f === "u" || f === "x" + ? parseInt(d.substring(2), 16) + : d.charCodeAt(1); + } + function g(d) { + if (d < 32) return (d < 16 ? "\\x0" : "\\x") + d.toString(16); + d = String.fromCharCode(d); + return d === "\\" || d === "-" || d === "]" || d === "^" ? "\\" + d : d; + } + function j(d) { + var a = d + .substring(1, d.length - 1) + .match( + /\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g + ), + d = [], + f = a[0] === "^", + b = ["["]; + f && b.push("^"); + for (var f = f ? 1 : 0, c = a.length; f < c; ++f) { + var h = a[f]; + if (/\\[bdsw]/i.test(h)) b.push(h); + else { + var h = i(h), + e; + f + 2 < c && "-" === a[f + 1] + ? ((e = i(a[f + 2])), (f += 2)) + : (e = h); + d.push([h, e]); + e < 65 || + h > 122 || + (e < 65 || + h > 90 || + d.push([Math.max(65, h) | 32, Math.min(e, 90) | 32]), + e < 97 || + h > 122 || + d.push([Math.max(97, h) & -33, Math.min(e, 122) & -33])); + } + } + d.sort(function(d, a) { + return d[0] - a[0] || a[1] - d[1]; + }); + a = []; + c = []; + for (f = 0; f < d.length; ++f) + (h = d[f]), + h[0] <= c[1] + 1 ? (c[1] = Math.max(c[1], h[1])) : a.push((c = h)); + for (f = 0; f < a.length; ++f) + (h = a[f]), + b.push(g(h[0])), + h[1] > h[0] && (h[1] + 1 > h[0] && b.push("-"), b.push(g(h[1]))); + b.push("]"); + return b.join(""); + } + function t(d) { + for ( + var a = d.source.match( + /\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g + ), + b = a.length, + i = [], + c = 0, + h = 0; + c < b; + ++c + ) { + var e = a[c]; + e === "(" + ? ++h + : "\\" === e.charAt(0) && + (e = +e.substring(1)) && + (e <= h ? (i[e] = -1) : (a[c] = g(e))); + } + for (c = 1; c < i.length; ++c) -1 === i[c] && (i[c] = ++z); + for (h = c = 0; c < b; ++c) + (e = a[c]), + e === "(" + ? (++h, i[h] || (a[c] = "(?:")) + : "\\" === e.charAt(0) && + (e = +e.substring(1)) && + e <= h && + (a[c] = "\\" + i[e]); + for (c = 0; c < b; ++c) "^" === a[c] && "^" !== a[c + 1] && (a[c] = ""); + if (d.ignoreCase && w) + for (c = 0; c < b; ++c) + (e = a[c]), + (d = e.charAt(0)), + e.length >= 2 && d === "[" + ? (a[c] = j(e)) + : d !== "\\" && + (a[c] = e.replace(/[A-Za-z]/g, function(d) { + d = d.charCodeAt(0); + return "[" + String.fromCharCode(d & -33, d | 32) + "]"; + })); + return a.join(""); + } + for (var z = 0, w = !1, k = !1, m = 0, b = a.length; m < b; ++m) { + var o = a[m]; + if (o.ignoreCase) k = !0; + else if ( + /[a-z]/i.test( + o.source.replace(/\\u[\da-f]{4}|\\x[\da-f]{2}|\\[^UXux]/gi, "") + ) + ) { + w = !0; + k = !1; + break; + } + } + for ( + var s = { + b: 8, + t: 9, + n: 10, + v: 11, + f: 12, + r: 13 + }, + q = [], + m = 0, + b = a.length; + m < b; + ++m + ) { + o = a[m]; + if (o.global || o.multiline) throw Error("" + o); + q.push("(?:" + t(o) + ")"); + } + return RegExp(q.join("|"), k ? "gi" : "g"); + } + function P(a, i) { + function g(a) { + switch (a.nodeType) { + case 1: + if (j.test(a.className)) break; + for (var b = a.firstChild; b; b = b.nextSibling) g(b); + b = a.nodeName.toLowerCase(); + if ("br" === b || "li" === b) + (t[k] = "\n"), (w[k << 1] = z++), (w[(k++ << 1) | 1] = a); + break; + case 3: + case 4: + (b = a.nodeValue), + b.length && + ((b = i + ? b.replace(/\r\n?/g, "\n") + : b.replace(/[\t\n\r ]+/g, " ")), + (t[k] = b), + (w[k << 1] = z), + (z += b.length), + (w[(k++ << 1) | 1] = a)); + } + } + var j = /(?:^|\s)nocode(?:\s|$)/, + t = [], + z = 0, + w = [], + k = 0; + g(a); + return { a: t.join("").replace(/\n$/, ""), d: w }; + } + function E(a, i, g, j) { + i && ((a = { a: i, e: a }), g(a), j.push.apply(j, a.g)); + } + function x(a, i) { + function g(a) { + for ( + var k = a.e, + m = [k, "pln"], + b = 0, + o = a.a.match(t) || [], + s = {}, + q = 0, + d = o.length; + q < d; + ++q + ) { + var v = o[q], + f = s[v], + u = void 0, + c; + if (typeof f === "string") c = !1; + else { + var h = j[v.charAt(0)]; + if (h) (u = v.match(h[1])), (f = h[0]); + else { + for (c = 0; c < z; ++c) + if (((h = i[c]), (u = v.match(h[1])))) { + f = h[0]; + break; + } + u || (f = "pln"); + } + if ( + (c = f.length >= 5 && "lang-" === f.substring(0, 5)) && + !(u && typeof u[1] === "string") + ) + (c = !1), (f = "src"); + c || (s[v] = f); + } + h = b; + b += v.length; + if (c) { + c = u[1]; + var e = v.indexOf(c), + p = e + c.length; + u[2] && ((p = v.length - u[2].length), (e = p - c.length)); + f = f.substring(5); + E(k + h, v.substring(0, e), g, m); + E(k + h + e, c, F(f, c), m); + E(k + h + p, v.substring(p), g, m); + } else m.push(k + h, f); + } + a.g = m; + } + var j = {}, + t; + (function() { + for ( + var g = a.concat(i), k = [], m = {}, b = 0, o = g.length; + b < o; + ++b + ) { + var s = g[b], + q = s[3]; + if (q) for (var d = q.length; --d >= 0; ) j[q.charAt(d)] = s; + s = s[1]; + q = "" + s; + m.hasOwnProperty(q) || (k.push(s), (m[q] = r)); + } + k.push(/[\S\s]/); + t = O(k); + })(); + var z = i.length; + return g; + } + function l(a) { + var i = [], + g = []; + a.tripleQuotedStrings + ? i.push([ + "str", + /^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/, + r, + "'\"" + ]) + : a.multiLineStrings + ? i.push([ + "str", + /^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, + r, + "'\"`" + ]) + : i.push([ + "str", + /^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/, + r, + "\"'" + ]); + a.verbatimStrings && g.push(["str", /^@"(?:[^"]|"")*(?:"|$)/, r]); + var j = a.hashComments; + j && + (a.cStyleComments + ? (j > 1 + ? i.push(["com", /^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/, r, "#"]) + : i.push([ + "com", + /^#(?:(?:define|e(?:l|nd)if|else|error|ifn?def|include|line|pragma|undef|warning)\b|[^\n\r]*)/, + r, + "#" + ]), + g.push([ + "str", + /^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h(?:h|pp|\+\+)?|[a-z]\w*)>/, + r + ])) + : i.push(["com", /^#[^\n\r]*/, r, "#"])); + a.cStyleComments && + (g.push(["com", /^\/\/[^\n\r]*/, r]), + g.push(["com", /^\/\*[\S\s]*?(?:\*\/|$)/, r])); + a.regexLiterals && + g.push([ + "lang-regex", + /^(?:^^\.?|[+-]|[!=]={0,2}|#|%=?|&&?=?|\(|\*=?|[+-]=|->|\/=?|::?|<{1,3}=?|[,;?@[{~]|\^\^?=?|\|\|?=?|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/ + ]); + (j = a.types) && g.push(["typ", j]); + a = ("" + a.keywords).replace(/^ | $/g, ""); + a.length && + g.push(["kwd", RegExp("^(?:" + a.replace(/[\s,]+/g, "|") + ")\\b"), r]); + i.push(["pln", /^\s+/, r, " \r\n\t\u00a0"]); + g.push( + ["lit", /^@[$_a-z][\w$@]*/i, r], + ["typ", /^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/, r], + ["pln", /^[$_a-z][\w$@]*/i, r], + [ + "lit", + /^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i, + r, + "0123456789" + ], + ["pln", /^\\[\S\s]?/, r], + ["pun", /^.[^\s\w"$'./@\\`]*/, r] + ); + return x(i, g); + } + function G(a, i, g) { + function j(a) { + switch (a.nodeType) { + case 1: + if (z.test(a.className)) break; + if ("br" === a.nodeName) + t(a), a.parentNode && a.parentNode.removeChild(a); + else for (a = a.firstChild; a; a = a.nextSibling) j(a); + break; + case 3: + case 4: + if (g) { + var b = a.nodeValue, + f = b.match(n); + if (f) { + var i = b.substring(0, f.index); + a.nodeValue = i; + (b = b.substring(f.index + f[0].length)) && + a.parentNode.insertBefore(k.createTextNode(b), a.nextSibling); + t(a); + i || a.parentNode.removeChild(a); + } + } + } + } + function t(a) { + function i(a, b) { + var d = b ? a.cloneNode(!1) : a, + e = a.parentNode; + if (e) { + var e = i(e, 1), + f = a.nextSibling; + e.appendChild(d); + for (var g = f; g; g = f) (f = g.nextSibling), e.appendChild(g); + } + return d; + } + for (; !a.nextSibling; ) if (((a = a.parentNode), !a)) return; + for ( + var a = i(a.nextSibling, 0), f; + (f = a.parentNode) && f.nodeType === 1; + + ) + a = f; + b.push(a); + } + for ( + var z = /(?:^|\s)nocode(?:\s|$)/, + n = /\r\n?|\n/, + k = a.ownerDocument, + m = k.createElement("li"); + a.firstChild; + + ) + m.appendChild(a.firstChild); + for (var b = [m], o = 0; o < b.length; ++o) j(b[o]); + i === (i | 0) && b[0].setAttribute("value", i); + var s = k.createElement("ol"); + s.className = "linenums"; + for (var i = Math.max(0, (i - 1) | 0) || 0, o = 0, q = b.length; o < q; ++o) + (m = b[o]), + (m.className = "L" + (o + i) % 10), + m.firstChild || m.appendChild(k.createTextNode("\u00a0")), + s.appendChild(m); + a.appendChild(s); + } + function n(a, i) { + for (var g = i.length; --g >= 0; ) { + var j = i[g]; + A.hasOwnProperty(j) + ? C.console && console.warn("cannot override language handler %s", j) + : (A[j] = a); + } + } + function F(a, i) { + if (!a || !A.hasOwnProperty(a)) + a = /^\s*= e && (j += 2); + g >= p && (s += 2); + } + } finally { + if (c) c.style.display = h; + } + } catch (A) { + C.console && console.log(A && A.stack ? A.stack : A); + } + } + var C = window, + y = ["break,continue,do,else,for,if,return,while"], + B = [ + [ + y, + "auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile" + ], + "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof" + ], + I = [ + B, + "alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where" + ], + J = [ + B, + "abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient" + ], + K = [ + J, + "as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,let,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var,virtual,where" + ], + B = [ + B, + "debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN" + ], + L = [ + y, + "and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None" + ], + M = [ + y, + "alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END" + ], + y = [y, "case,done,elif,esac,eval,fi,function,in,local,set,then,until"], + N = /^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)\b/, + Q = /\S/, + R = l({ + keywords: [ + I, + K, + B, + "caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END" + + L, + M, + y + ], + hashComments: !0, + cStyleComments: !0, + multiLineStrings: !0, + regexLiterals: !0 + }), + A = {}; + n(R, ["default-code"]); + n( + x( + [], + [ + ["pln", /^[^]*(?:>|$)/], + ["com", /^<\!--[\S\s]*?(?:--\>|$)/], + ["lang-", /^<\?([\S\s]+?)(?:\?>|$)/], + ["lang-", /^<%([\S\s]+?)(?:%>|$)/], + ["pun", /^(?:<[%?]|[%?]>)/], + ["lang-", /^]*>([\S\s]+?)<\/xmp\b[^>]*>/i], + ["lang-js", /^]*>([\S\s]*?)(<\/script\b[^>]*>)/i], + ["lang-css", /^]*>([\S\s]*?)(<\/style\b[^>]*>)/i], + ["lang-in.tag", /^(<\/?[a-z][^<>]*>)/i] + ] + ), + ["default-markup", "htm", "html", "mxml", "xhtml", "xml", "xsl"] + ); + n( + x( + [ + ["pln", /^\s+/, r, " \t\r\n"], + ["atv", /^(?:"[^"]*"?|'[^']*'?)/, r, "\"'"] + ], + [ + ["tag", /^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i], + ["atn", /^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i], + ["lang-uq.val", /^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/], + ["pun", /^[/<->]+/], + ["lang-js", /^on\w+\s*=\s*"([^"]+)"/i], + ["lang-js", /^on\w+\s*=\s*'([^']+)'/i], + ["lang-js", /^on\w+\s*=\s*([^\s"'>]+)/i], + ["lang-css", /^style\s*=\s*"([^"]+)"/i], + ["lang-css", /^style\s*=\s*'([^']+)'/i], + ["lang-css", /^style\s*=\s*([^\s"'>]+)/i] + ] + ), + ["in.tag"] + ); + n(x([], [["atv", /^[\S\s]+/]]), ["uq.val"]); + n(l({ keywords: I, hashComments: !0, cStyleComments: !0, types: N }), [ + "c", + "cc", + "cpp", + "cxx", + "cyc", + "m" + ]); + n(l({ keywords: "null,true,false" }), ["json"]); + n( + l({ + keywords: K, + hashComments: !0, + cStyleComments: !0, + verbatimStrings: !0, + types: N + }), + ["cs"] + ); + n(l({ keywords: J, cStyleComments: !0 }), ["java"]); + n(l({ keywords: y, hashComments: !0, multiLineStrings: !0 }), [ + "bsh", + "csh", + "sh" + ]); + n( + l({ + keywords: L, + hashComments: !0, + multiLineStrings: !0, + tripleQuotedStrings: !0 + }), + ["cv", "py"] + ); + n( + l({ + keywords: + "caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END", + hashComments: !0, + multiLineStrings: !0, + regexLiterals: !0 + }), + ["perl", "pl", "pm"] + ); + n( + l({ + keywords: M, + hashComments: !0, + multiLineStrings: !0, + regexLiterals: !0 + }), + ["rb"] + ); + n(l({ keywords: B, cStyleComments: !0, regexLiterals: !0 }), ["js"]); + n( + l({ + keywords: + "all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,throw,true,try,unless,until,when,while,yes", + hashComments: 3, + cStyleComments: !0, + multilineStrings: !0, + tripleQuotedStrings: !0, + regexLiterals: !0 + }), + ["coffee"] + ); + n(x([], [["str", /^[\S\s]+/]]), ["regex"]); + var S = (C.PR = { + createSimpleLexer: x, + registerLangHandler: n, + sourceDecorator: l, + PR_ATTRIB_NAME: "atn", + PR_ATTRIB_VALUE: "atv", + PR_COMMENT: "com", + PR_DECLARATION: "dec", + PR_KEYWORD: "kwd", + PR_LITERAL: "lit", + PR_NOCODE: "nocode", + PR_PLAIN: "pln", + PR_PUNCTUATION: "pun", + PR_SOURCE: "src", + PR_STRING: "str", + PR_TAG: "tag", + PR_TYPE: "typ", + prettyPrintOne: (C.prettyPrintOne = function(a, i, g) { + var j = document.createElement("pre"); + j.innerHTML = a; + g && G(j, g, !0); + H({ h: i, j: g, c: j, i: 1 }); + return j.innerHTML; + }), + prettyPrint: (C.prettyPrint = function(a) { + function i() { + var u; + for ( + var g = C.PR_SHOULD_USE_CONTINUATION ? k.now() + 250 : Infinity; + m < j.length && k.now() < g; + m++ + ) { + var c = j[m], + h = c.className; + if (s.test(h) && !q.test(h)) { + for (var e = !1, p = c.parentNode; p; p = p.parentNode) + if (f.test(p.tagName) && p.className && s.test(p.className)) { + e = !0; + break; + } + if (!e) { + c.className += " prettyprinted"; + var h = h.match(o), + n; + if ((e = !h)) { + for ( + var e = c, p = void 0, l = e.firstChild; + l; + l = l.nextSibling + ) + var t = l.nodeType, + p = + t === 1 + ? p + ? e + : l + : t === 3 + ? Q.test(l.nodeValue) + ? e + : p + : p; + e = (n = p === e ? void 0 : p) && v.test(n.tagName); + } + e && (h = n.className.match(o)); + h && (h = h[1]); + (u = d.test(c.tagName) + ? 1 + : (e = (e = c.currentStyle) + ? e.whiteSpace + : document.defaultView && + document.defaultView.getComputedStyle + ? document.defaultView + .getComputedStyle(c, r) + .getPropertyValue("white-space") + : 0) && "pre" === e.substring(0, 3)), + (e = u); + (p = (p = c.className.match(/\blinenums\b(?::(\d+))?/)) + ? p[1] && p[1].length + ? +p[1] + : !0 + : !1) && G(c, p, e); + b = { h: h, c: c, j: p, i: e }; + H(b); + } + } + } + m < j.length ? setTimeout(i, 250) : a && a(); + } + for ( + var g = [ + document.getElementsByTagName("pre"), + document.getElementsByTagName("code"), + document.getElementsByTagName("xmp") + ], + j = [], + n = 0; + n < g.length; + ++n + ) + for (var l = 0, w = g[n].length; l < w; ++l) j.push(g[n][l]); + var g = r, + k = Date; + k.now || + (k = { + now: function() { + return +new Date(); + } + }); + var m = 0, + b, + o = /\blang(?:uage)?-([\w.]+)(?!\S)/, + s = /\bprettyprint\b/, + q = /\bprettyprinted\b/, + d = /pre|xmp/i, + v = /^code$/i, + f = /^(?:pre|code|xmp)$/i; + i(); + }) + }); + typeof define === "function" && + define.amd && + define("google-code-prettify", [], function() { + return S; + }); +})(); diff --git a/kirby/vendor/filp/whoops/src/Whoops/Resources/js/whoops.base.js b/kirby/vendor/filp/whoops/src/Whoops/Resources/js/whoops.base.js new file mode 100755 index 0000000..5e5623f --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Resources/js/whoops.base.js @@ -0,0 +1,208 @@ +Zepto(function($) { + var $leftPanel = $(".left-panel"); + var $frameContainer = $(".frames-container"); + var $appFramesTab = $("#application-frames-tab"); + var $allFramesTab = $("#all-frames-tab"); + var $container = $(".details-container"); + var $activeLine = $frameContainer.find(".frame.active"); + var $activeFrame = $container.find(".frame-code.active"); + var $ajaxEditors = $(".editor-link[data-ajax]"); + var $header = $("header"); + + $header.on("mouseenter", function() { + if ($header.find(".exception").height() >= 145) { + $header.addClass("header-expand"); + } + }); + $header.on("mouseleave", function() { + $header.removeClass("header-expand"); + }); + + /* + * add prettyprint classes to our current active codeblock + * run prettyPrint() to highlight the active code + * scroll to the line when prettyprint is done + * highlight the current line + */ + var renderCurrentCodeblock = function(id) { + // remove previous codeblocks so we only render the active one + $(".code-block").removeClass("prettyprint"); + + // pass the id in when we can for speed + if (typeof id === "undefined" || typeof id === "object") { + var id = /frame\-line\-([\d]*)/.exec($activeLine.attr("id"))[1]; + } + + $("#frame-code-linenums-" + id).addClass("prettyprint"); + $("#frame-code-args-" + id).addClass("prettyprint"); + + prettyPrint(highlightCurrentLine); + }; + + /* + * Highlight the active and neighboring lines for the current frame + * Adjust the offset to make sure that line is veritcally centered + */ + + var highlightCurrentLine = function() { + var activeLineNumber = +$activeLine.find(".frame-line").text(); + var $lines = $activeFrame.find(".linenums li"); + var firstLine = +$lines.first().val(); + + // We show more code than needed, purely for proper syntax highlighting + // Let’s hide a big chunk of that code and then scroll the remaining block + $activeFrame + .find(".code-block") + .first() + .css({ + maxHeight: 345, + overflow: "hidden" + }); + + var $offset = $($lines[activeLineNumber - firstLine - 10]); + if ($offset.length > 0) { + $offset[0].scrollIntoView(); + } + + $($lines[activeLineNumber - firstLine - 1]).addClass("current"); + $($lines[activeLineNumber - firstLine]).addClass("current active"); + $($lines[activeLineNumber - firstLine + 1]).addClass("current"); + + $container.scrollTop(0); + }; + + /* + * click handler for loading codeblocks + */ + + $frameContainer.on("click", ".frame", function() { + var $this = $(this); + var id = /frame\-line\-([\d]*)/.exec($this.attr("id"))[1]; + var $codeFrame = $("#frame-code-" + id); + + if ($codeFrame) { + $activeLine.removeClass("active"); + $activeFrame.removeClass("active"); + + $this.addClass("active"); + $codeFrame.addClass("active"); + + $activeLine = $this; + $activeFrame = $codeFrame; + + renderCurrentCodeblock(id); + } + }); + + var clipboard = new Clipboard(".clipboard"); + var showTooltip = function(elem, msg) { + elem.setAttribute("class", "clipboard tooltipped tooltipped-s"); + elem.setAttribute("aria-label", msg); + }; + + clipboard.on("success", function(e) { + e.clearSelection(); + + showTooltip(e.trigger, "Copied!"); + }); + + clipboard.on("error", function(e) { + showTooltip(e.trigger, fallbackMessage(e.action)); + }); + + var btn = document.querySelector(".clipboard"); + + btn.addEventListener("mouseleave", function(e) { + e.currentTarget.setAttribute("class", "clipboard"); + e.currentTarget.removeAttribute("aria-label"); + }); + + function fallbackMessage(action) { + var actionMsg = ""; + var actionKey = action === "cut" ? "X" : "C"; + + if (/Mac/i.test(navigator.userAgent)) { + actionMsg = "Press ⌘-" + actionKey + " to " + action; + } else { + actionMsg = "Press Ctrl-" + actionKey + " to " + action; + } + + return actionMsg; + } + + function scrollIntoView($node, $parent) { + var nodeOffset = $node.offset(); + var nodeTop = nodeOffset.top; + var nodeBottom = nodeTop + nodeOffset.height; + var parentScrollTop = $parent.scrollTop(); + var parentHeight = $parent.height(); + + if (nodeTop < 0) { + $parent.scrollTop(parentScrollTop + nodeTop); + } else if (nodeBottom > parentHeight) { + $parent.scrollTop(parentScrollTop + nodeBottom - parentHeight); + } + } + + $(document).on("keydown", function(e) { + var applicationFrames = $frameContainer.hasClass( + "frames-container-application" + ), + frameClass = applicationFrames ? ".frame.frame-application" : ".frame"; + + if (e.ctrlKey || e.which === 74 || e.which === 75) { + // CTRL+Arrow-UP/k and Arrow-Down/j support: + // 1) select the next/prev element + // 2) make sure the newly selected element is within the view-scope + // 3) focus the (right) container, so arrow-up/down (without ctrl) scroll the details + if (e.which === 38 /* arrow up */ || e.which === 75 /* k */) { + $activeLine.prev(frameClass).click(); + scrollIntoView($activeLine, $leftPanel); + $container.focus(); + e.preventDefault(); + } else if (e.which === 40 /* arrow down */ || e.which === 74 /* j */) { + $activeLine.next(frameClass).click(); + scrollIntoView($activeLine, $leftPanel); + $container.focus(); + e.preventDefault(); + } + } else if (e.which == 78 /* n */) { + if ($appFramesTab.length) { + setActiveFramesTab($(".frames-tab:not(.frames-tab-active)")); + } + } + }); + + // Render late enough for highlightCurrentLine to be ready + renderCurrentCodeblock(); + + // Avoid to quit the page with some protocol (e.g. IntelliJ Platform REST API) + $ajaxEditors.on("click", function(e) { + e.preventDefault(); + $.get(this.href); + }); + + // Symfony VarDumper: Close the by default expanded objects + $(".sf-dump-expanded") + .removeClass("sf-dump-expanded") + .addClass("sf-dump-compact"); + $(".sf-dump-toggle span").html("▶"); + + // Make the given frames-tab active + function setActiveFramesTab($tab) { + $tab.addClass("frames-tab-active"); + + if ($tab.attr("id") == "application-frames-tab") { + $frameContainer.addClass("frames-container-application"); + $allFramesTab.removeClass("frames-tab-active"); + } else { + $frameContainer.removeClass("frames-container-application"); + $appFramesTab.removeClass("frames-tab-active"); + } + } + + $("a.frames-tab").on("click", function(e) { + e.preventDefault(); + setActiveFramesTab($(this)); + }); +}); diff --git a/kirby/vendor/filp/whoops/src/Whoops/Resources/js/zepto.min.js b/kirby/vendor/filp/whoops/src/Whoops/Resources/js/zepto.min.js new file mode 100755 index 0000000..161fd3f --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Resources/js/zepto.min.js @@ -0,0 +1,1547 @@ +/* Zepto v1.1.3 - zepto event ajax form ie - zeptojs.com/license */ +var Zepto = (function() { + function L(t) { + return null == t ? String(t) : j[T.call(t)] || "object"; + } + function Z(t) { + return "function" == L(t); + } + function $(t) { + return null != t && t == t.window; + } + function _(t) { + return null != t && t.nodeType == t.DOCUMENT_NODE; + } + function D(t) { + return "object" == L(t); + } + function R(t) { + return D(t) && !$(t) && Object.getPrototypeOf(t) == Object.prototype; + } + function M(t) { + return "number" == typeof t.length; + } + function k(t) { + return s.call(t, function(t) { + return null != t; + }); + } + function z(t) { + return t.length > 0 ? n.fn.concat.apply([], t) : t; + } + function F(t) { + return t + .replace(/::/g, "/") + .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2") + .replace(/([a-z\d])([A-Z])/g, "$1_$2") + .replace(/_/g, "-") + .toLowerCase(); + } + function q(t) { + return t in f ? f[t] : (f[t] = new RegExp("(^|\\s)" + t + "(\\s|$)")); + } + function H(t, e) { + return "number" != typeof e || c[F(t)] ? e : e + "px"; + } + function I(t) { + var e, n; + return ( + u[t] || + ((e = a.createElement(t)), + a.body.appendChild(e), + (n = getComputedStyle(e, "").getPropertyValue("display")), + e.parentNode.removeChild(e), + "none" == n && (n = "block"), + (u[t] = n)), + u[t] + ); + } + function V(t) { + return "children" in t + ? o.call(t.children) + : n.map(t.childNodes, function(t) { + return 1 == t.nodeType ? t : void 0; + }); + } + function U(n, i, r) { + for (e in i) + r && (R(i[e]) || A(i[e])) + ? (R(i[e]) && !R(n[e]) && (n[e] = {}), + A(i[e]) && !A(n[e]) && (n[e] = []), + U(n[e], i[e], r)) + : i[e] !== t && (n[e] = i[e]); + } + function B(t, e) { + return null == e ? n(t) : n(t).filter(e); + } + function J(t, e, n, i) { + return Z(e) ? e.call(t, n, i) : e; + } + function X(t, e, n) { + null == n ? t.removeAttribute(e) : t.setAttribute(e, n); + } + function W(e, n) { + var i = e.className, + r = i && i.baseVal !== t; + return n === t + ? r + ? i.baseVal + : i + : void (r ? (i.baseVal = n) : (e.className = n)); + } + function Y(t) { + var e; + try { + return t + ? "true" == t || + ("false" == t + ? !1 + : "null" == t + ? null + : /^0/.test(t) || isNaN((e = Number(t))) + ? /^[\[\{]/.test(t) + ? n.parseJSON(t) + : t + : e) + : t; + } catch (i) { + return t; + } + } + function G(t, e) { + e(t); + for (var n in t.childNodes) G(t.childNodes[n], e); + } + var t, + e, + n, + i, + C, + N, + r = [], + o = r.slice, + s = r.filter, + a = window.document, + u = {}, + f = {}, + c = { + "column-count": 1, + columns: 1, + "font-weight": 1, + "line-height": 1, + opacity: 1, + "z-index": 1, + zoom: 1 + }, + l = /^\s*<(\w+|!)[^>]*>/, + h = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, + p = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, + d = /^(?:body|html)$/i, + m = /([A-Z])/g, + g = ["val", "css", "html", "text", "data", "width", "height", "offset"], + v = ["after", "prepend", "before", "append"], + y = a.createElement("table"), + x = a.createElement("tr"), + b = { + tr: a.createElement("tbody"), + tbody: y, + thead: y, + tfoot: y, + td: x, + th: x, + "*": a.createElement("div") + }, + w = /complete|loaded|interactive/, + E = /^[\w-]*$/, + j = {}, + T = j.toString, + S = {}, + O = a.createElement("div"), + P = { + tabindex: "tabIndex", + readonly: "readOnly", + for: "htmlFor", + class: "className", + maxlength: "maxLength", + cellspacing: "cellSpacing", + cellpadding: "cellPadding", + rowspan: "rowSpan", + colspan: "colSpan", + usemap: "useMap", + frameborder: "frameBorder", + contenteditable: "contentEditable" + }, + A = + Array.isArray || + function(t) { + return t instanceof Array; + }; + return ( + (S.matches = function(t, e) { + if (!e || !t || 1 !== t.nodeType) return !1; + var n = + t.webkitMatchesSelector || + t.mozMatchesSelector || + t.oMatchesSelector || + t.matchesSelector; + if (n) return n.call(t, e); + var i, + r = t.parentNode, + o = !r; + return ( + o && (r = O).appendChild(t), + (i = ~S.qsa(r, e).indexOf(t)), + o && O.removeChild(t), + i + ); + }), + (C = function(t) { + return t.replace(/-+(.)?/g, function(t, e) { + return e ? e.toUpperCase() : ""; + }); + }), + (N = function(t) { + return s.call(t, function(e, n) { + return t.indexOf(e) == n; + }); + }), + (S.fragment = function(e, i, r) { + var s, u, f; + return ( + h.test(e) && (s = n(a.createElement(RegExp.$1))), + s || + (e.replace && (e = e.replace(p, "<$1>")), + i === t && (i = l.test(e) && RegExp.$1), + i in b || (i = "*"), + (f = b[i]), + (f.innerHTML = "" + e), + (s = n.each(o.call(f.childNodes), function() { + f.removeChild(this); + }))), + R(r) && + ((u = n(s)), + n.each(r, function(t, e) { + g.indexOf(t) > -1 ? u[t](e) : u.attr(t, e); + })), + s + ); + }), + (S.Z = function(t, e) { + return (t = t || []), (t.__proto__ = n.fn), (t.selector = e || ""), t; + }), + (S.isZ = function(t) { + return t instanceof S.Z; + }), + (S.init = function(e, i) { + var r; + if (!e) return S.Z(); + if ("string" == typeof e) + if (((e = e.trim()), "<" == e[0] && l.test(e))) + (r = S.fragment(e, RegExp.$1, i)), (e = null); + else { + if (i !== t) return n(i).find(e); + r = S.qsa(a, e); + } + else { + if (Z(e)) return n(a).ready(e); + if (S.isZ(e)) return e; + if (A(e)) r = k(e); + else if (D(e)) (r = [e]), (e = null); + else if (l.test(e)) + (r = S.fragment(e.trim(), RegExp.$1, i)), (e = null); + else { + if (i !== t) return n(i).find(e); + r = S.qsa(a, e); + } + } + return S.Z(r, e); + }), + (n = function(t, e) { + return S.init(t, e); + }), + (n.extend = function(t) { + var e, + n = o.call(arguments, 1); + return ( + "boolean" == typeof t && ((e = t), (t = n.shift())), + n.forEach(function(n) { + U(t, n, e); + }), + t + ); + }), + (S.qsa = function(t, e) { + var n, + i = "#" == e[0], + r = !i && "." == e[0], + s = i || r ? e.slice(1) : e, + a = E.test(s); + return _(t) && a && i + ? (n = t.getElementById(s)) + ? [n] + : [] + : 1 !== t.nodeType && 9 !== t.nodeType + ? [] + : o.call( + a && !i + ? r + ? t.getElementsByClassName(s) + : t.getElementsByTagName(e) + : t.querySelectorAll(e) + ); + }), + (n.contains = function(t, e) { + return t !== e && t.contains(e); + }), + (n.type = L), + (n.isFunction = Z), + (n.isWindow = $), + (n.isArray = A), + (n.isPlainObject = R), + (n.isEmptyObject = function(t) { + var e; + for (e in t) return !1; + return !0; + }), + (n.inArray = function(t, e, n) { + return r.indexOf.call(e, t, n); + }), + (n.camelCase = C), + (n.trim = function(t) { + return null == t ? "" : String.prototype.trim.call(t); + }), + (n.uuid = 0), + (n.support = {}), + (n.expr = {}), + (n.map = function(t, e) { + var n, + r, + o, + i = []; + if (M(t)) + for (r = 0; r < t.length; r++) (n = e(t[r], r)), null != n && i.push(n); + else for (o in t) (n = e(t[o], o)), null != n && i.push(n); + return z(i); + }), + (n.each = function(t, e) { + var n, i; + if (M(t)) { + for (n = 0; n < t.length; n++) + if (e.call(t[n], n, t[n]) === !1) return t; + } else for (i in t) if (e.call(t[i], i, t[i]) === !1) return t; + return t; + }), + (n.grep = function(t, e) { + return s.call(t, e); + }), + window.JSON && (n.parseJSON = JSON.parse), + n.each( + "Boolean Number String Function Array Date RegExp Object Error".split( + " " + ), + function(t, e) { + j["[object " + e + "]"] = e.toLowerCase(); + } + ), + (n.fn = { + forEach: r.forEach, + reduce: r.reduce, + push: r.push, + sort: r.sort, + indexOf: r.indexOf, + concat: r.concat, + map: function(t) { + return n( + n.map(this, function(e, n) { + return t.call(e, n, e); + }) + ); + }, + slice: function() { + return n(o.apply(this, arguments)); + }, + ready: function(t) { + return ( + w.test(a.readyState) && a.body + ? t(n) + : a.addEventListener( + "DOMContentLoaded", + function() { + t(n); + }, + !1 + ), + this + ); + }, + get: function(e) { + return e === t ? o.call(this) : this[e >= 0 ? e : e + this.length]; + }, + toArray: function() { + return this.get(); + }, + size: function() { + return this.length; + }, + remove: function() { + return this.each(function() { + null != this.parentNode && this.parentNode.removeChild(this); + }); + }, + each: function(t) { + return ( + r.every.call(this, function(e, n) { + return t.call(e, n, e) !== !1; + }), + this + ); + }, + filter: function(t) { + return Z(t) + ? this.not(this.not(t)) + : n( + s.call(this, function(e) { + return S.matches(e, t); + }) + ); + }, + add: function(t, e) { + return n(N(this.concat(n(t, e)))); + }, + is: function(t) { + return this.length > 0 && S.matches(this[0], t); + }, + not: function(e) { + var i = []; + if (Z(e) && e.call !== t) + this.each(function(t) { + e.call(this, t) || i.push(this); + }); + else { + var r = + "string" == typeof e + ? this.filter(e) + : M(e) && Z(e.item) + ? o.call(e) + : n(e); + this.forEach(function(t) { + r.indexOf(t) < 0 && i.push(t); + }); + } + return n(i); + }, + has: function(t) { + return this.filter(function() { + return D(t) + ? n.contains(this, t) + : n(this) + .find(t) + .size(); + }); + }, + eq: function(t) { + return -1 === t ? this.slice(t) : this.slice(t, +t + 1); + }, + first: function() { + var t = this[0]; + return t && !D(t) ? t : n(t); + }, + last: function() { + var t = this[this.length - 1]; + return t && !D(t) ? t : n(t); + }, + find: function(t) { + var e, + i = this; + return (e = + "object" == typeof t + ? n(t).filter(function() { + var t = this; + return r.some.call(i, function(e) { + return n.contains(e, t); + }); + }) + : 1 == this.length + ? n(S.qsa(this[0], t)) + : this.map(function() { + return S.qsa(this, t); + })); + }, + closest: function(t, e) { + var i = this[0], + r = !1; + for ( + "object" == typeof t && (r = n(t)); + i && !(r ? r.indexOf(i) >= 0 : S.matches(i, t)); + + ) + i = i !== e && !_(i) && i.parentNode; + return n(i); + }, + parents: function(t) { + for (var e = [], i = this; i.length > 0; ) + i = n.map(i, function(t) { + return (t = t.parentNode) && !_(t) && e.indexOf(t) < 0 + ? (e.push(t), t) + : void 0; + }); + return B(e, t); + }, + parent: function(t) { + return B(N(this.pluck("parentNode")), t); + }, + children: function(t) { + return B( + this.map(function() { + return V(this); + }), + t + ); + }, + contents: function() { + return this.map(function() { + return o.call(this.childNodes); + }); + }, + siblings: function(t) { + return B( + this.map(function(t, e) { + return s.call(V(e.parentNode), function(t) { + return t !== e; + }); + }), + t + ); + }, + empty: function() { + return this.each(function() { + this.innerHTML = ""; + }); + }, + pluck: function(t) { + return n.map(this, function(e) { + return e[t]; + }); + }, + show: function() { + return this.each(function() { + "none" == this.style.display && (this.style.display = ""), + "none" == getComputedStyle(this, "").getPropertyValue("display") && + (this.style.display = I(this.nodeName)); + }); + }, + replaceWith: function(t) { + return this.before(t).remove(); + }, + wrap: function(t) { + var e = Z(t); + if (this[0] && !e) + var i = n(t).get(0), + r = i.parentNode || this.length > 1; + return this.each(function(o) { + n(this).wrapAll(e ? t.call(this, o) : r ? i.cloneNode(!0) : i); + }); + }, + wrapAll: function(t) { + if (this[0]) { + n(this[0]).before((t = n(t))); + for (var e; (e = t.children()).length; ) t = e.first(); + n(t).append(this); + } + return this; + }, + wrapInner: function(t) { + var e = Z(t); + return this.each(function(i) { + var r = n(this), + o = r.contents(), + s = e ? t.call(this, i) : t; + o.length ? o.wrapAll(s) : r.append(s); + }); + }, + unwrap: function() { + return ( + this.parent().each(function() { + n(this).replaceWith(n(this).children()); + }), + this + ); + }, + clone: function() { + return this.map(function() { + return this.cloneNode(!0); + }); + }, + hide: function() { + return this.css("display", "none"); + }, + toggle: function(e) { + return this.each(function() { + var i = n(this); + (e === t ? "none" == i.css("display") : e) ? i.show() : i.hide(); + }); + }, + prev: function(t) { + return n(this.pluck("previousElementSibling")).filter(t || "*"); + }, + next: function(t) { + return n(this.pluck("nextElementSibling")).filter(t || "*"); + }, + html: function(t) { + return 0 === arguments.length + ? this.length > 0 + ? this[0].innerHTML + : null + : this.each(function(e) { + var i = this.innerHTML; + n(this) + .empty() + .append(J(this, t, e, i)); + }); + }, + text: function(e) { + return 0 === arguments.length + ? this.length > 0 + ? this[0].textContent + : null + : this.each(function() { + this.textContent = e === t ? "" : "" + e; + }); + }, + attr: function(n, i) { + var r; + return "string" == typeof n && i === t + ? 0 == this.length || 1 !== this[0].nodeType + ? t + : "value" == n && "INPUT" == this[0].nodeName + ? this.val() + : !(r = this[0].getAttribute(n)) && n in this[0] + ? this[0][n] + : r + : this.each(function(t) { + if (1 === this.nodeType) + if (D(n)) for (e in n) X(this, e, n[e]); + else X(this, n, J(this, i, t, this.getAttribute(n))); + }); + }, + removeAttr: function(t) { + return this.each(function() { + 1 === this.nodeType && X(this, t); + }); + }, + prop: function(e, n) { + return ( + (e = P[e] || e), + n === t + ? this[0] && this[0][e] + : this.each(function(t) { + this[e] = J(this, n, t, this[e]); + }) + ); + }, + data: function(e, n) { + var i = this.attr("data-" + e.replace(m, "-$1").toLowerCase(), n); + return null !== i ? Y(i) : t; + }, + val: function(t) { + return 0 === arguments.length + ? this[0] && + (this[0].multiple + ? n(this[0]) + .find("option") + .filter(function() { + return this.selected; + }) + .pluck("value") + : this[0].value) + : this.each(function(e) { + this.value = J(this, t, e, this.value); + }); + }, + offset: function(t) { + if (t) + return this.each(function(e) { + var i = n(this), + r = J(this, t, e, i.offset()), + o = i.offsetParent().offset(), + s = { top: r.top - o.top, left: r.left - o.left }; + "static" == i.css("position") && (s.position = "relative"), + i.css(s); + }); + if (0 == this.length) return null; + var e = this[0].getBoundingClientRect(); + return { + left: e.left + window.pageXOffset, + top: e.top + window.pageYOffset, + width: Math.round(e.width), + height: Math.round(e.height) + }; + }, + css: function(t, i) { + if (arguments.length < 2) { + var r = this[0], + o = getComputedStyle(r, ""); + if (!r) return; + if ("string" == typeof t) + return r.style[C(t)] || o.getPropertyValue(t); + if (A(t)) { + var s = {}; + return ( + n.each(A(t) ? t : [t], function(t, e) { + s[e] = r.style[C(e)] || o.getPropertyValue(e); + }), + s + ); + } + } + var a = ""; + if ("string" == L(t)) + i || 0 === i + ? (a = F(t) + ":" + H(t, i)) + : this.each(function() { + this.style.removeProperty(F(t)); + }); + else + for (e in t) + t[e] || 0 === t[e] + ? (a += F(e) + ":" + H(e, t[e]) + ";") + : this.each(function() { + this.style.removeProperty(F(e)); + }); + return this.each(function() { + this.style.cssText += ";" + a; + }); + }, + index: function(t) { + return t + ? this.indexOf(n(t)[0]) + : this.parent() + .children() + .indexOf(this[0]); + }, + hasClass: function(t) { + return t + ? r.some.call( + this, + function(t) { + return this.test(W(t)); + }, + q(t) + ) + : !1; + }, + addClass: function(t) { + return t + ? this.each(function(e) { + i = []; + var r = W(this), + o = J(this, t, e, r); + o.split(/\s+/g).forEach(function(t) { + n(this).hasClass(t) || i.push(t); + }, this), + i.length && W(this, r + (r ? " " : "") + i.join(" ")); + }) + : this; + }, + removeClass: function(e) { + return this.each(function(n) { + return e === t + ? W(this, "") + : ((i = W(this)), + J(this, e, n, i) + .split(/\s+/g) + .forEach(function(t) { + i = i.replace(q(t), " "); + }), + void W(this, i.trim())); + }); + }, + toggleClass: function(e, i) { + return e + ? this.each(function(r) { + var o = n(this), + s = J(this, e, r, W(this)); + s.split(/\s+/g).forEach(function(e) { + (i === t + ? !o.hasClass(e) + : i) + ? o.addClass(e) + : o.removeClass(e); + }); + }) + : this; + }, + scrollTop: function(e) { + if (this.length) { + var n = "scrollTop" in this[0]; + return e === t + ? n + ? this[0].scrollTop + : this[0].pageYOffset + : this.each( + n + ? function() { + this.scrollTop = e; + } + : function() { + this.scrollTo(this.scrollX, e); + } + ); + } + }, + scrollLeft: function(e) { + if (this.length) { + var n = "scrollLeft" in this[0]; + return e === t + ? n + ? this[0].scrollLeft + : this[0].pageXOffset + : this.each( + n + ? function() { + this.scrollLeft = e; + } + : function() { + this.scrollTo(e, this.scrollY); + } + ); + } + }, + position: function() { + if (this.length) { + var t = this[0], + e = this.offsetParent(), + i = this.offset(), + r = d.test(e[0].nodeName) ? { top: 0, left: 0 } : e.offset(); + return ( + (i.top -= parseFloat(n(t).css("margin-top")) || 0), + (i.left -= parseFloat(n(t).css("margin-left")) || 0), + (r.top += parseFloat(n(e[0]).css("border-top-width")) || 0), + (r.left += parseFloat(n(e[0]).css("border-left-width")) || 0), + { top: i.top - r.top, left: i.left - r.left } + ); + } + }, + offsetParent: function() { + return this.map(function() { + for ( + var t = this.offsetParent || a.body; + t && !d.test(t.nodeName) && "static" == n(t).css("position"); + + ) + t = t.offsetParent; + return t; + }); + } + }), + (n.fn.detach = n.fn.remove), + ["width", "height"].forEach(function(e) { + var i = e.replace(/./, function(t) { + return t[0].toUpperCase(); + }); + n.fn[e] = function(r) { + var o, + s = this[0]; + return r === t + ? $(s) + ? s["inner" + i] + : _(s) + ? s.documentElement["scroll" + i] + : (o = this.offset()) && o[e] + : this.each(function(t) { + (s = n(this)), s.css(e, J(this, r, t, s[e]())); + }); + }; + }), + v.forEach(function(t, e) { + var i = e % 2; + (n.fn[t] = function() { + var t, + o, + r = n.map(arguments, function(e) { + return ( + (t = L(e)), + "object" == t || "array" == t || null == e ? e : S.fragment(e) + ); + }), + s = this.length > 1; + return r.length < 1 + ? this + : this.each(function(t, a) { + (o = i ? a : a.parentNode), + (a = + 0 == e + ? a.nextSibling + : 1 == e + ? a.firstChild + : 2 == e + ? a + : null), + r.forEach(function(t) { + if (s) t = t.cloneNode(!0); + else if (!o) return n(t).remove(); + G(o.insertBefore(t, a), function(t) { + null == t.nodeName || + "SCRIPT" !== t.nodeName.toUpperCase() || + (t.type && "text/javascript" !== t.type) || + t.src || + window.eval.call(window, t.innerHTML); + }); + }); + }); + }), + (n.fn[i ? t + "To" : "insert" + (e ? "Before" : "After")] = function( + e + ) { + return n(e)[t](this), this; + }); + }), + (S.Z.prototype = n.fn), + (S.uniq = N), + (S.deserializeValue = Y), + (n.zepto = S), + n + ); +})(); +(window.Zepto = Zepto), + void 0 === window.$ && (window.$ = Zepto), + (function(t) { + function l(t) { + return t._zid || (t._zid = e++); + } + function h(t, e, n, i) { + if (((e = p(e)), e.ns)) var r = d(e.ns); + return (s[l(t)] || []).filter(function(t) { + return !( + !t || + (e.e && t.e != e.e) || + (e.ns && !r.test(t.ns)) || + (n && l(t.fn) !== l(n)) || + (i && t.sel != i) + ); + }); + } + function p(t) { + var e = ("" + t).split("."); + return { + e: e[0], + ns: e + .slice(1) + .sort() + .join(" ") + }; + } + function d(t) { + return new RegExp("(?:^| )" + t.replace(" ", " .* ?") + "(?: |$)"); + } + function m(t, e) { + return (t.del && !u && t.e in f) || !!e; + } + function g(t) { + return c[t] || (u && f[t]) || t; + } + function v(e, i, r, o, a, u, f) { + var h = l(e), + d = s[h] || (s[h] = []); + i.split(/\s/).forEach(function(i) { + if ("ready" == i) return t(document).ready(r); + var s = p(i); + (s.fn = r), + (s.sel = a), + s.e in c && + (r = function(e) { + var n = e.relatedTarget; + return !n || (n !== this && !t.contains(this, n)) + ? s.fn.apply(this, arguments) + : void 0; + }), + (s.del = u); + var l = u || r; + (s.proxy = function(t) { + if (((t = j(t)), !t.isImmediatePropagationStopped())) { + t.data = o; + var i = l.apply(e, t._args == n ? [t] : [t].concat(t._args)); + return i === !1 && (t.preventDefault(), t.stopPropagation()), i; + } + }), + (s.i = d.length), + d.push(s), + "addEventListener" in e && + e.addEventListener(g(s.e), s.proxy, m(s, f)); + }); + } + function y(t, e, n, i, r) { + var o = l(t); + (e || "").split(/\s/).forEach(function(e) { + h(t, e, n, i).forEach(function(e) { + delete s[o][e.i], + "removeEventListener" in t && + t.removeEventListener(g(e.e), e.proxy, m(e, r)); + }); + }); + } + function j(e, i) { + return ( + (i || !e.isDefaultPrevented) && + (i || (i = e), + t.each(E, function(t, n) { + var r = i[t]; + (e[t] = function() { + return (this[n] = x), r && r.apply(i, arguments); + }), + (e[n] = b); + }), + (i.defaultPrevented !== n + ? i.defaultPrevented + : "returnValue" in i + ? i.returnValue === !1 + : i.getPreventDefault && i.getPreventDefault()) && + (e.isDefaultPrevented = x)), + e + ); + } + function T(t) { + var e, + i = { originalEvent: t }; + for (e in t) w.test(e) || t[e] === n || (i[e] = t[e]); + return j(i, t); + } + var n, + e = 1, + i = Array.prototype.slice, + r = t.isFunction, + o = function(t) { + return "string" == typeof t; + }, + s = {}, + a = {}, + u = "onfocusin" in window, + f = { focus: "focusin", blur: "focusout" }, + c = { mouseenter: "mouseover", mouseleave: "mouseout" }; + (a.click = a.mousedown = a.mouseup = a.mousemove = "MouseEvents"), + (t.event = { add: v, remove: y }), + (t.proxy = function(e, n) { + if (r(e)) { + var i = function() { + return e.apply(n, arguments); + }; + return (i._zid = l(e)), i; + } + if (o(n)) return t.proxy(e[n], e); + throw new TypeError("expected function"); + }), + (t.fn.bind = function(t, e, n) { + return this.on(t, e, n); + }), + (t.fn.unbind = function(t, e) { + return this.off(t, e); + }), + (t.fn.one = function(t, e, n, i) { + return this.on(t, e, n, i, 1); + }); + var x = function() { + return !0; + }, + b = function() { + return !1; + }, + w = /^([A-Z]|returnValue$|layer[XY]$)/, + E = { + preventDefault: "isDefaultPrevented", + stopImmediatePropagation: "isImmediatePropagationStopped", + stopPropagation: "isPropagationStopped" + }; + (t.fn.delegate = function(t, e, n) { + return this.on(e, t, n); + }), + (t.fn.undelegate = function(t, e, n) { + return this.off(e, t, n); + }), + (t.fn.live = function(e, n) { + return t(document.body).delegate(this.selector, e, n), this; + }), + (t.fn.die = function(e, n) { + return t(document.body).undelegate(this.selector, e, n), this; + }), + (t.fn.on = function(e, s, a, u, f) { + var c, + l, + h = this; + return e && !o(e) + ? (t.each(e, function(t, e) { + h.on(t, s, a, e, f); + }), + h) + : (o(s) || r(u) || u === !1 || ((u = a), (a = s), (s = n)), + (r(a) || a === !1) && ((u = a), (a = n)), + u === !1 && (u = b), + h.each(function(n, r) { + f && + (c = function(t) { + return y(r, t.type, u), u.apply(this, arguments); + }), + s && + (l = function(e) { + var n, + o = t(e.target) + .closest(s, r) + .get(0); + return o && o !== r + ? ((n = t.extend(T(e), { + currentTarget: o, + liveFired: r + })), + (c || u).apply(o, [n].concat(i.call(arguments, 1)))) + : void 0; + }), + v(r, e, u, a, s, l || c); + })); + }), + (t.fn.off = function(e, i, s) { + var a = this; + return e && !o(e) + ? (t.each(e, function(t, e) { + a.off(t, i, e); + }), + a) + : (o(i) || r(s) || s === !1 || ((s = i), (i = n)), + s === !1 && (s = b), + a.each(function() { + y(this, e, s, i); + })); + }), + (t.fn.trigger = function(e, n) { + return ( + (e = o(e) || t.isPlainObject(e) ? t.Event(e) : j(e)), + (e._args = n), + this.each(function() { + "dispatchEvent" in this + ? this.dispatchEvent(e) + : t(this).triggerHandler(e, n); + }) + ); + }), + (t.fn.triggerHandler = function(e, n) { + var i, r; + return ( + this.each(function(s, a) { + (i = T(o(e) ? t.Event(e) : e)), + (i._args = n), + (i.target = a), + t.each(h(a, e.type || e), function(t, e) { + return ( + (r = e.proxy(i)), + i.isImmediatePropagationStopped() ? !1 : void 0 + ); + }); + }), + r + ); + }), + "focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select keydown keypress keyup error" + .split(" ") + .forEach(function(e) { + t.fn[e] = function(t) { + return t ? this.bind(e, t) : this.trigger(e); + }; + }), + ["focus", "blur"].forEach(function(e) { + t.fn[e] = function(t) { + return ( + t + ? this.bind(e, t) + : this.each(function() { + try { + this[e](); + } catch (t) {} + }), + this + ); + }; + }), + (t.Event = function(t, e) { + o(t) || ((e = t), (t = e.type)); + var n = document.createEvent(a[t] || "Events"), + i = !0; + if (e) for (var r in e) "bubbles" == r ? (i = !!e[r]) : (n[r] = e[r]); + return n.initEvent(t, i, !0), j(n); + }); + })(Zepto), + (function(t) { + function l(e, n, i) { + var r = t.Event(n); + return t(e).trigger(r, i), !r.isDefaultPrevented(); + } + function h(t, e, i, r) { + return t.global ? l(e || n, i, r) : void 0; + } + function p(e) { + e.global && 0 === t.active++ && h(e, null, "ajaxStart"); + } + function d(e) { + e.global && !--t.active && h(e, null, "ajaxStop"); + } + function m(t, e) { + var n = e.context; + return e.beforeSend.call(n, t, e) === !1 || + h(e, n, "ajaxBeforeSend", [t, e]) === !1 + ? !1 + : void h(e, n, "ajaxSend", [t, e]); + } + function g(t, e, n, i) { + var r = n.context, + o = "success"; + n.success.call(r, t, o, e), + i && i.resolveWith(r, [t, o, e]), + h(n, r, "ajaxSuccess", [e, n, t]), + y(o, e, n); + } + function v(t, e, n, i, r) { + var o = i.context; + i.error.call(o, n, e, t), + r && r.rejectWith(o, [n, e, t]), + h(i, o, "ajaxError", [n, i, t || e]), + y(e, n, i); + } + function y(t, e, n) { + var i = n.context; + n.complete.call(i, e, t), h(n, i, "ajaxComplete", [e, n]), d(n); + } + function x() {} + function b(t) { + return ( + t && (t = t.split(";", 2)[0]), + (t && + (t == f + ? "html" + : t == u + ? "json" + : s.test(t) + ? "script" + : a.test(t) && "xml")) || + "text" + ); + } + function w(t, e) { + return "" == e ? t : (t + "&" + e).replace(/[&?]{1,2}/, "?"); + } + function E(e) { + e.processData && + e.data && + "string" != t.type(e.data) && + (e.data = t.param(e.data, e.traditional)), + !e.data || + (e.type && "GET" != e.type.toUpperCase()) || + ((e.url = w(e.url, e.data)), (e.data = void 0)); + } + function j(e, n, i, r) { + return ( + t.isFunction(n) && ((r = i), (i = n), (n = void 0)), + t.isFunction(i) || ((r = i), (i = void 0)), + { url: e, data: n, success: i, dataType: r } + ); + } + function S(e, n, i, r) { + var o, + s = t.isArray(n), + a = t.isPlainObject(n); + t.each(n, function(n, u) { + (o = t.type(u)), + r && + (n = i + ? r + : r + "[" + (a || "object" == o || "array" == o ? n : "") + "]"), + !r && s + ? e.add(u.name, u.value) + : "array" == o || (!i && "object" == o) + ? S(e, u, i, n) + : e.add(n, u); + }); + } + var i, + r, + e = 0, + n = window.document, + o = /)<[^<]*)*<\/script>/gi, + s = /^(?:text|application)\/javascript/i, + a = /^(?:text|application)\/xml/i, + u = "application/json", + f = "text/html", + c = /^\s*$/; + (t.active = 0), + (t.ajaxJSONP = function(i, r) { + if (!("type" in i)) return t.ajax(i); + var f, + h, + o = i.jsonpCallback, + s = (t.isFunction(o) ? o() : o) || "jsonp" + ++e, + a = n.createElement("script"), + u = window[s], + c = function(e) { + t(a).triggerHandler("error", e || "abort"); + }, + l = { abort: c }; + return ( + r && r.promise(l), + t(a).on("load error", function(e, n) { + clearTimeout(h), + t(a) + .off() + .remove(), + "error" != e.type && f + ? g(f[0], l, i, r) + : v(null, n || "error", l, i, r), + (window[s] = u), + f && t.isFunction(u) && u(f[0]), + (u = f = void 0); + }), + m(l, i) === !1 + ? (c("abort"), l) + : ((window[s] = function() { + f = arguments; + }), + (a.src = i.url.replace(/\?(.+)=\?/, "?$1=" + s)), + n.head.appendChild(a), + i.timeout > 0 && + (h = setTimeout(function() { + c("timeout"); + }, i.timeout)), + l) + ); + }), + (t.ajaxSettings = { + type: "GET", + beforeSend: x, + success: x, + error: x, + complete: x, + context: null, + global: !0, + xhr: function() { + return new window.XMLHttpRequest(); + }, + accepts: { + script: + "text/javascript, application/javascript, application/x-javascript", + json: u, + xml: "application/xml, text/xml", + html: f, + text: "text/plain" + }, + crossDomain: !1, + timeout: 0, + processData: !0, + cache: !0 + }), + (t.ajax = function(e) { + var n = t.extend({}, e || {}), + o = t.Deferred && t.Deferred(); + for (i in t.ajaxSettings) void 0 === n[i] && (n[i] = t.ajaxSettings[i]); + p(n), + n.crossDomain || + (n.crossDomain = + /^([\w-]+:)?\/\/([^\/]+)/.test(n.url) && + RegExp.$2 != window.location.host), + n.url || (n.url = window.location.toString()), + E(n), + n.cache === !1 && (n.url = w(n.url, "_=" + Date.now())); + var s = n.dataType, + a = /\?.+=\?/.test(n.url); + if ("jsonp" == s || a) + return ( + a || + (n.url = w( + n.url, + n.jsonp ? n.jsonp + "=?" : n.jsonp === !1 ? "" : "callback=?" + )), + t.ajaxJSONP(n, o) + ); + var j, + u = n.accepts[s], + f = {}, + l = function(t, e) { + f[t.toLowerCase()] = [t, e]; + }, + h = /^([\w-]+:)\/\//.test(n.url) + ? RegExp.$1 + : window.location.protocol, + d = n.xhr(), + y = d.setRequestHeader; + if ( + (o && o.promise(d), + n.crossDomain || l("X-Requested-With", "XMLHttpRequest"), + l("Accept", u || "*/*"), + (u = n.mimeType || u) && + (u.indexOf(",") > -1 && (u = u.split(",", 2)[0]), + d.overrideMimeType && d.overrideMimeType(u)), + (n.contentType || + (n.contentType !== !1 && + n.data && + "GET" != n.type.toUpperCase())) && + l( + "Content-Type", + n.contentType || "application/x-www-form-urlencoded" + ), + n.headers) + ) + for (r in n.headers) l(r, n.headers[r]); + if ( + ((d.setRequestHeader = l), + (d.onreadystatechange = function() { + if (4 == d.readyState) { + (d.onreadystatechange = x), clearTimeout(j); + var e, + i = !1; + if ( + (d.status >= 200 && d.status < 300) || + 304 == d.status || + (0 == d.status && "file:" == h) + ) { + (s = s || b(n.mimeType || d.getResponseHeader("content-type"))), + (e = d.responseText); + try { + "script" == s + ? (1, eval)(e) + : "xml" == s + ? (e = d.responseXML) + : "json" == s && (e = c.test(e) ? null : t.parseJSON(e)); + } catch (r) { + i = r; + } + i ? v(i, "parsererror", d, n, o) : g(e, d, n, o); + } else + v(d.statusText || null, d.status ? "error" : "abort", d, n, o); + } + }), + m(d, n) === !1) + ) + return d.abort(), v(null, "abort", d, n, o), d; + if (n.xhrFields) for (r in n.xhrFields) d[r] = n.xhrFields[r]; + var T = "async" in n ? n.async : !0; + d.open(n.type, n.url, T, n.username, n.password); + for (r in f) y.apply(d, f[r]); + return ( + n.timeout > 0 && + (j = setTimeout(function() { + (d.onreadystatechange = x), + d.abort(), + v(null, "timeout", d, n, o); + }, n.timeout)), + d.send(n.data ? n.data : null), + d + ); + }), + (t.get = function() { + return t.ajax(j.apply(null, arguments)); + }), + (t.post = function() { + var e = j.apply(null, arguments); + return (e.type = "POST"), t.ajax(e); + }), + (t.getJSON = function() { + var e = j.apply(null, arguments); + return (e.dataType = "json"), t.ajax(e); + }), + (t.fn.load = function(e, n, i) { + if (!this.length) return this; + var a, + r = this, + s = e.split(/\s/), + u = j(e, n, i), + f = u.success; + return ( + s.length > 1 && ((u.url = s[0]), (a = s[1])), + (u.success = function(e) { + r.html( + a + ? t("
") + .html(e.replace(o, "")) + .find(a) + : e + ), + f && f.apply(r, arguments); + }), + t.ajax(u), + this + ); + }); + var T = encodeURIComponent; + t.param = function(t, e) { + var n = []; + return ( + (n.add = function(t, e) { + this.push(T(t) + "=" + T(e)); + }), + S(n, t, e), + n.join("&").replace(/%20/g, "+") + ); + }; + })(Zepto), + (function(t) { + (t.fn.serializeArray = function() { + var n, + e = []; + return ( + t([].slice.call(this.get(0).elements)).each(function() { + n = t(this); + var i = n.attr("type"); + "fieldset" != this.nodeName.toLowerCase() && + !this.disabled && + "submit" != i && + "reset" != i && + "button" != i && + (("radio" != i && "checkbox" != i) || this.checked) && + e.push({ name: n.attr("name"), value: n.val() }); + }), + e + ); + }), + (t.fn.serialize = function() { + var t = []; + return ( + this.serializeArray().forEach(function(e) { + t.push( + encodeURIComponent(e.name) + "=" + encodeURIComponent(e.value) + ); + }), + t.join("&") + ); + }), + (t.fn.submit = function(e) { + if (e) this.bind("submit", e); + else if (this.length) { + var n = t.Event("submit"); + this.eq(0).trigger(n), n.isDefaultPrevented() || this.get(0).submit(); + } + return this; + }); + })(Zepto), + (function(t) { + "__proto__" in {} || + t.extend(t.zepto, { + Z: function(e, n) { + return ( + (e = e || []), + t.extend(e, t.fn), + (e.selector = n || ""), + (e.__Z = !0), + e + ); + }, + isZ: function(e) { + return "array" === t.type(e) && "__Z" in e; + } + }); + try { + getComputedStyle(void 0); + } catch (e) { + var n = getComputedStyle; + window.getComputedStyle = function(t) { + try { + return n(t); + } catch (e) { + return null; + } + }; + } + })(Zepto); diff --git a/kirby/vendor/filp/whoops/src/Whoops/Resources/views/env_details.html.php b/kirby/vendor/filp/whoops/src/Whoops/Resources/views/env_details.html.php new file mode 100755 index 0000000..8db1493 --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Resources/views/env_details.html.php @@ -0,0 +1,42 @@ + +
+

Environment & details:

+ +
+ $data): ?> +
+ + + + + + + + + + $value): ?> + + + + + +
KeyValue
escape($k) ?>dump($value) ?>
+ + + empty + +
+ +
+ + +
+ + $handler): ?> +
+ . escape(get_class($handler)) ?> +
+ +
+ +
diff --git a/kirby/vendor/filp/whoops/src/Whoops/Resources/views/frame_code.html.php b/kirby/vendor/filp/whoops/src/Whoops/Resources/views/frame_code.html.php new file mode 100755 index 0000000..b7717d7 --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Resources/views/frame_code.html.php @@ -0,0 +1,63 @@ + +
+ $frame): ?> + getLine(); ?> +
+ + getFileLines($line - 20, 40); + + // getFileLines can return null if there is no source code + if ($range): + $range = array_map(function ($line) { return empty($line) ? ' ' : $line;}, $range); + $start = key($range) + 1; + $code = join("\n", $range); + ?> +
escape($code) ?>
+ + + + + dumpArgs($frame); ?> + +
+ Arguments +
+
+ +
+ + + getComments(); + ?> +
+ $comment): ?> + +
+ escape($context) ?> + escapeButPreserveUris($comment) ?> +
+ +
+ +
+ +
diff --git a/kirby/vendor/filp/whoops/src/Whoops/Resources/views/frame_list.html.php b/kirby/vendor/filp/whoops/src/Whoops/Resources/views/frame_list.html.php new file mode 100755 index 0000000..a4bc338 --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Resources/views/frame_list.html.php @@ -0,0 +1,17 @@ + + $frame): ?> +
+ +
+ breakOnDelimiter('\\', $tpl->escape($frame->getClass() ?: '')) ?> + breakOnDelimiter('\\', $tpl->escape($frame->getFunction() ?: '')) ?> +
+ +
+ getFile() ? $tpl->breakOnDelimiter('/', $tpl->shorten($tpl->escape($frame->getFile()))) : '<#unknown>' ?>getLine() ?> +
+
+"> + render($frame_list) ?> +
\ No newline at end of file diff --git a/kirby/vendor/filp/whoops/src/Whoops/Resources/views/frames_description.html.php b/kirby/vendor/filp/whoops/src/Whoops/Resources/views/frames_description.html.php new file mode 100755 index 0000000..e32cf88 --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Resources/views/frames_description.html.php @@ -0,0 +1,20 @@ +
+ + + + Application frames (countIsApplication() ?>) + + + + Application frames (countIsApplication() ?>) + + + + All frames () + + + + Stack frames () + + +
diff --git a/kirby/vendor/filp/whoops/src/Whoops/Resources/views/header.html.php b/kirby/vendor/filp/whoops/src/Whoops/Resources/views/header.html.php new file mode 100755 index 0000000..aefbeac --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Resources/views/header.html.php @@ -0,0 +1,93 @@ +
+
+ $nameSection): ?> + + escape($nameSection) ?> + + escape($nameSection) . ' \\' ?> + + + + (escape($code) ?>) + +
+ +
+ + escape($message) ?> + + + +
+ Previous exceptions +
+ +
    + $previousMessage): ?> +
  • + escape($previousMessage) ?> + () +
  • + +
+ + + + + + No message + + + + + escape($plain_exception) ?> + +
+
diff --git a/kirby/vendor/filp/whoops/src/Whoops/Resources/views/header_outer.html.php b/kirby/vendor/filp/whoops/src/Whoops/Resources/views/header_outer.html.php new file mode 100755 index 0000000..f682cbb --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Resources/views/header_outer.html.php @@ -0,0 +1,3 @@ +
+ render($header) ?> +
diff --git a/kirby/vendor/filp/whoops/src/Whoops/Resources/views/layout.html.php b/kirby/vendor/filp/whoops/src/Whoops/Resources/views/layout.html.php new file mode 100755 index 0000000..6b676cc --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Resources/views/layout.html.php @@ -0,0 +1,33 @@ + + + + + + + + <?php echo $tpl->escape($page_title) ?> + + + + + +
+
+ + render($panel_left_outer) ?> + + render($panel_details_outer) ?> + +
+
+ + + + + + + diff --git a/kirby/vendor/filp/whoops/src/Whoops/Resources/views/panel_details.html.php b/kirby/vendor/filp/whoops/src/Whoops/Resources/views/panel_details.html.php new file mode 100755 index 0000000..a85e451 --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Resources/views/panel_details.html.php @@ -0,0 +1,2 @@ +render($frame_code) ?> +render($env_details) ?> \ No newline at end of file diff --git a/kirby/vendor/filp/whoops/src/Whoops/Resources/views/panel_details_outer.html.php b/kirby/vendor/filp/whoops/src/Whoops/Resources/views/panel_details_outer.html.php new file mode 100755 index 0000000..8162d8c --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Resources/views/panel_details_outer.html.php @@ -0,0 +1,3 @@ +
+ render($panel_details) ?> +
\ No newline at end of file diff --git a/kirby/vendor/filp/whoops/src/Whoops/Resources/views/panel_left.html.php b/kirby/vendor/filp/whoops/src/Whoops/Resources/views/panel_left.html.php new file mode 100755 index 0000000..7e652e4 --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Resources/views/panel_left.html.php @@ -0,0 +1,4 @@ +render($header_outer); +$tpl->render($frames_description); +$tpl->render($frames_container); diff --git a/kirby/vendor/filp/whoops/src/Whoops/Resources/views/panel_left_outer.html.php b/kirby/vendor/filp/whoops/src/Whoops/Resources/views/panel_left_outer.html.php new file mode 100755 index 0000000..77b575c --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Resources/views/panel_left_outer.html.php @@ -0,0 +1,3 @@ +
+ render($panel_left) ?> +
\ No newline at end of file diff --git a/kirby/vendor/filp/whoops/src/Whoops/Run.php b/kirby/vendor/filp/whoops/src/Whoops/Run.php new file mode 100755 index 0000000..1d51f1c --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Run.php @@ -0,0 +1,410 @@ + + */ + +namespace Whoops; + +use InvalidArgumentException; +use Whoops\Exception\ErrorException; +use Whoops\Exception\Inspector; +use Whoops\Handler\CallbackHandler; +use Whoops\Handler\Handler; +use Whoops\Handler\HandlerInterface; +use Whoops\Util\Misc; +use Whoops\Util\SystemFacade; + +final class Run implements RunInterface +{ + private $isRegistered; + private $allowQuit = true; + private $sendOutput = true; + + /** + * @var integer|false + */ + private $sendHttpCode = 500; + + /** + * @var HandlerInterface[] + */ + private $handlerStack = []; + + private $silencedPatterns = []; + + private $system; + + public function __construct(SystemFacade $system = null) + { + $this->system = $system ?: new SystemFacade; + } + + /** + * Pushes a handler to the end of the stack + * + * @throws InvalidArgumentException If argument is not callable or instance of HandlerInterface + * @param Callable|HandlerInterface $handler + * @return Run + */ + public function pushHandler($handler) + { + if (is_callable($handler)) { + $handler = new CallbackHandler($handler); + } + + if (!$handler instanceof HandlerInterface) { + throw new InvalidArgumentException( + "Argument to " . __METHOD__ . " must be a callable, or instance of " + . "Whoops\\Handler\\HandlerInterface" + ); + } + + $this->handlerStack[] = $handler; + return $this; + } + + /** + * Removes the last handler in the stack and returns it. + * Returns null if there"s nothing else to pop. + * @return null|HandlerInterface + */ + public function popHandler() + { + return array_pop($this->handlerStack); + } + + /** + * Returns an array with all handlers, in the + * order they were added to the stack. + * @return array + */ + public function getHandlers() + { + return $this->handlerStack; + } + + /** + * Clears all handlers in the handlerStack, including + * the default PrettyPage handler. + * @return Run + */ + public function clearHandlers() + { + $this->handlerStack = []; + return $this; + } + + /** + * @param \Throwable $exception + * @return Inspector + */ + private function getInspector($exception) + { + return new Inspector($exception); + } + + /** + * Registers this instance as an error handler. + * @return Run + */ + public function register() + { + if (!$this->isRegistered) { + // Workaround PHP bug 42098 + // https://bugs.php.net/bug.php?id=42098 + class_exists("\\Whoops\\Exception\\ErrorException"); + class_exists("\\Whoops\\Exception\\FrameCollection"); + class_exists("\\Whoops\\Exception\\Frame"); + class_exists("\\Whoops\\Exception\\Inspector"); + + $this->system->setErrorHandler([$this, self::ERROR_HANDLER]); + $this->system->setExceptionHandler([$this, self::EXCEPTION_HANDLER]); + $this->system->registerShutdownFunction([$this, self::SHUTDOWN_HANDLER]); + + $this->isRegistered = true; + } + + return $this; + } + + /** + * Unregisters all handlers registered by this Whoops\Run instance + * @return Run + */ + public function unregister() + { + if ($this->isRegistered) { + $this->system->restoreExceptionHandler(); + $this->system->restoreErrorHandler(); + + $this->isRegistered = false; + } + + return $this; + } + + /** + * Should Whoops allow Handlers to force the script to quit? + * @param bool|int $exit + * @return bool + */ + public function allowQuit($exit = null) + { + if (func_num_args() == 0) { + return $this->allowQuit; + } + + return $this->allowQuit = (bool) $exit; + } + + /** + * Silence particular errors in particular files + * @param array|string $patterns List or a single regex pattern to match + * @param int $levels Defaults to E_STRICT | E_DEPRECATED + * @return \Whoops\Run + */ + public function silenceErrorsInPaths($patterns, $levels = 10240) + { + $this->silencedPatterns = array_merge( + $this->silencedPatterns, + array_map( + function ($pattern) use ($levels) { + return [ + "pattern" => $pattern, + "levels" => $levels, + ]; + }, + (array) $patterns + ) + ); + return $this; + } + + + /** + * Returns an array with silent errors in path configuration + * + * @return array + */ + public function getSilenceErrorsInPaths() + { + return $this->silencedPatterns; + } + + /* + * Should Whoops send HTTP error code to the browser if possible? + * Whoops will by default send HTTP code 500, but you may wish to + * use 502, 503, or another 5xx family code. + * + * @param bool|int $code + * @return int|false + */ + public function sendHttpCode($code = null) + { + if (func_num_args() == 0) { + return $this->sendHttpCode; + } + + if (!$code) { + return $this->sendHttpCode = false; + } + + if ($code === true) { + $code = 500; + } + + if ($code < 400 || 600 <= $code) { + throw new InvalidArgumentException( + "Invalid status code '$code', must be 4xx or 5xx" + ); + } + + return $this->sendHttpCode = $code; + } + + /** + * Should Whoops push output directly to the client? + * If this is false, output will be returned by handleException + * @param bool|int $send + * @return bool + */ + public function writeToOutput($send = null) + { + if (func_num_args() == 0) { + return $this->sendOutput; + } + + return $this->sendOutput = (bool) $send; + } + + /** + * Handles an exception, ultimately generating a Whoops error + * page. + * + * @param \Throwable $exception + * @return string Output generated by handlers + */ + public function handleException($exception) + { + // Walk the registered handlers in the reverse order + // they were registered, and pass off the exception + $inspector = $this->getInspector($exception); + + // Capture output produced while handling the exception, + // we might want to send it straight away to the client, + // or return it silently. + $this->system->startOutputBuffering(); + + // Just in case there are no handlers: + $handlerResponse = null; + $handlerContentType = null; + + foreach (array_reverse($this->handlerStack) as $handler) { + $handler->setRun($this); + $handler->setInspector($inspector); + $handler->setException($exception); + + // The HandlerInterface does not require an Exception passed to handle() + // and neither of our bundled handlers use it. + // However, 3rd party handlers may have already relied on this parameter, + // and removing it would be possibly breaking for users. + $handlerResponse = $handler->handle($exception); + + // Collect the content type for possible sending in the headers. + $handlerContentType = method_exists($handler, 'contentType') ? $handler->contentType() : null; + + if (in_array($handlerResponse, [Handler::LAST_HANDLER, Handler::QUIT])) { + // The Handler has handled the exception in some way, and + // wishes to quit execution (Handler::QUIT), or skip any + // other handlers (Handler::LAST_HANDLER). If $this->allowQuit + // is false, Handler::QUIT behaves like Handler::LAST_HANDLER + break; + } + } + + $willQuit = $handlerResponse == Handler::QUIT && $this->allowQuit(); + + $output = $this->system->cleanOutputBuffer(); + + // If we're allowed to, send output generated by handlers directly + // to the output, otherwise, and if the script doesn't quit, return + // it so that it may be used by the caller + if ($this->writeToOutput()) { + // @todo Might be able to clean this up a bit better + if ($willQuit) { + // Cleanup all other output buffers before sending our output: + while ($this->system->getOutputBufferLevel() > 0) { + $this->system->endOutputBuffering(); + } + + // Send any headers if needed: + if (Misc::canSendHeaders() && $handlerContentType) { + header("Content-Type: {$handlerContentType}"); + } + } + + $this->writeToOutputNow($output); + } + + if ($willQuit) { + // HHVM fix for https://github.com/facebook/hhvm/issues/4055 + $this->system->flushOutputBuffer(); + + $this->system->stopExecution(1); + } + + return $output; + } + + /** + * Converts generic PHP errors to \ErrorException + * instances, before passing them off to be handled. + * + * This method MUST be compatible with set_error_handler. + * + * @param int $level + * @param string $message + * @param string $file + * @param int $line + * + * @return bool + * @throws ErrorException + */ + public function handleError($level, $message, $file = null, $line = null) + { + if ($level & $this->system->getErrorReportingLevel()) { + foreach ($this->silencedPatterns as $entry) { + $pathMatches = (bool) preg_match($entry["pattern"], $file); + $levelMatches = $level & $entry["levels"]; + if ($pathMatches && $levelMatches) { + // Ignore the error, abort handling + // See https://github.com/filp/whoops/issues/418 + return true; + } + } + + // XXX we pass $level for the "code" param only for BC reasons. + // see https://github.com/filp/whoops/issues/267 + $exception = new ErrorException($message, /*code*/ $level, /*severity*/ $level, $file, $line); + if ($this->canThrowExceptions) { + throw $exception; + } else { + $this->handleException($exception); + } + // Do not propagate errors which were already handled by Whoops. + return true; + } + + // Propagate error to the next handler, allows error_get_last() to + // work on silenced errors. + return false; + } + + /** + * Special case to deal with Fatal errors and the like. + */ + public function handleShutdown() + { + // If we reached this step, we are in shutdown handler. + // An exception thrown in a shutdown handler will not be propagated + // to the exception handler. Pass that information along. + $this->canThrowExceptions = false; + + $error = $this->system->getLastError(); + if ($error && Misc::isLevelFatal($error['type'])) { + // If there was a fatal error, + // it was not handled in handleError yet. + $this->handleError( + $error['type'], + $error['message'], + $error['file'], + $error['line'] + ); + } + } + + /** + * In certain scenarios, like in shutdown handler, we can not throw exceptions + * @var bool + */ + private $canThrowExceptions = true; + + /** + * Echo something to the browser + * @param string $output + * @return $this + */ + private function writeToOutputNow($output) + { + if ($this->sendHttpCode() && \Whoops\Util\Misc::canSendHeaders()) { + $this->system->setHttpResponseCode( + $this->sendHttpCode() + ); + } + + echo $output; + + return $this; + } +} diff --git a/kirby/vendor/filp/whoops/src/Whoops/RunInterface.php b/kirby/vendor/filp/whoops/src/Whoops/RunInterface.php new file mode 100755 index 0000000..67ba90d --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/RunInterface.php @@ -0,0 +1,131 @@ + + */ + +namespace Whoops; + +use InvalidArgumentException; +use Whoops\Exception\ErrorException; +use Whoops\Handler\HandlerInterface; + +interface RunInterface +{ + const EXCEPTION_HANDLER = "handleException"; + const ERROR_HANDLER = "handleError"; + const SHUTDOWN_HANDLER = "handleShutdown"; + + /** + * Pushes a handler to the end of the stack + * + * @throws InvalidArgumentException If argument is not callable or instance of HandlerInterface + * @param Callable|HandlerInterface $handler + * @return Run + */ + public function pushHandler($handler); + + /** + * Removes the last handler in the stack and returns it. + * Returns null if there"s nothing else to pop. + * + * @return null|HandlerInterface + */ + public function popHandler(); + + /** + * Returns an array with all handlers, in the + * order they were added to the stack. + * + * @return array + */ + public function getHandlers(); + + /** + * Clears all handlers in the handlerStack, including + * the default PrettyPage handler. + * + * @return Run + */ + public function clearHandlers(); + + /** + * Registers this instance as an error handler. + * + * @return Run + */ + public function register(); + + /** + * Unregisters all handlers registered by this Whoops\Run instance + * + * @return Run + */ + public function unregister(); + + /** + * Should Whoops allow Handlers to force the script to quit? + * + * @param bool|int $exit + * @return bool + */ + public function allowQuit($exit = null); + + /** + * Silence particular errors in particular files + * + * @param array|string $patterns List or a single regex pattern to match + * @param int $levels Defaults to E_STRICT | E_DEPRECATED + * @return \Whoops\Run + */ + public function silenceErrorsInPaths($patterns, $levels = 10240); + + /** + * Should Whoops send HTTP error code to the browser if possible? + * Whoops will by default send HTTP code 500, but you may wish to + * use 502, 503, or another 5xx family code. + * + * @param bool|int $code + * @return int|false + */ + public function sendHttpCode($code = null); + + /** + * Should Whoops push output directly to the client? + * If this is false, output will be returned by handleException + * + * @param bool|int $send + * @return bool + */ + public function writeToOutput($send = null); + + /** + * Handles an exception, ultimately generating a Whoops error + * page. + * + * @param \Throwable $exception + * @return string Output generated by handlers + */ + public function handleException($exception); + + /** + * Converts generic PHP errors to \ErrorException + * instances, before passing them off to be handled. + * + * This method MUST be compatible with set_error_handler. + * + * @param int $level + * @param string $message + * @param string $file + * @param int $line + * + * @return bool + * @throws ErrorException + */ + public function handleError($level, $message, $file = null, $line = null); + + /** + * Special case to deal with Fatal errors and the like. + */ + public function handleShutdown(); +} diff --git a/kirby/vendor/filp/whoops/src/Whoops/Util/HtmlDumperOutput.php b/kirby/vendor/filp/whoops/src/Whoops/Util/HtmlDumperOutput.php new file mode 100755 index 0000000..8c828fd --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Util/HtmlDumperOutput.php @@ -0,0 +1,36 @@ + + */ + +namespace Whoops\Util; + +/** + * Used as output callable for Symfony\Component\VarDumper\Dumper\HtmlDumper::dump() + * + * @see TemplateHelper::dump() + */ +class HtmlDumperOutput +{ + private $output; + + public function __invoke($line, $depth) + { + // A negative depth means "end of dump" + if ($depth >= 0) { + // Adds a two spaces indentation to the line + $this->output .= str_repeat(' ', $depth) . $line . "\n"; + } + } + + public function getOutput() + { + return $this->output; + } + + public function clear() + { + $this->output = null; + } +} diff --git a/kirby/vendor/filp/whoops/src/Whoops/Util/Misc.php b/kirby/vendor/filp/whoops/src/Whoops/Util/Misc.php new file mode 100755 index 0000000..001a687 --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Util/Misc.php @@ -0,0 +1,77 @@ + + */ + +namespace Whoops\Util; + +class Misc +{ + /** + * Can we at this point in time send HTTP headers? + * + * Currently this checks if we are even serving an HTTP request, + * as opposed to running from a command line. + * + * If we are serving an HTTP request, we check if it's not too late. + * + * @return bool + */ + public static function canSendHeaders() + { + return isset($_SERVER["REQUEST_URI"]) && !headers_sent(); + } + + public static function isAjaxRequest() + { + return ( + !empty($_SERVER['HTTP_X_REQUESTED_WITH']) + && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'); + } + + /** + * Check, if possible, that this execution was triggered by a command line. + * @return bool + */ + public static function isCommandLine() + { + return PHP_SAPI == 'cli'; + } + + /** + * Translate ErrorException code into the represented constant. + * + * @param int $error_code + * @return string + */ + public static function translateErrorCode($error_code) + { + $constants = get_defined_constants(true); + if (array_key_exists('Core', $constants)) { + foreach ($constants['Core'] as $constant => $value) { + if (substr($constant, 0, 2) == 'E_' && $value == $error_code) { + return $constant; + } + } + } + return "E_UNKNOWN"; + } + + /** + * Determine if an error level is fatal (halts execution) + * + * @param int $level + * @return bool + */ + public static function isLevelFatal($level) + { + $errors = E_ERROR; + $errors |= E_PARSE; + $errors |= E_CORE_ERROR; + $errors |= E_CORE_WARNING; + $errors |= E_COMPILE_ERROR; + $errors |= E_COMPILE_WARNING; + return ($level & $errors) > 0; + } +} diff --git a/kirby/vendor/filp/whoops/src/Whoops/Util/SystemFacade.php b/kirby/vendor/filp/whoops/src/Whoops/Util/SystemFacade.php new file mode 100755 index 0000000..cc82e7f --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Util/SystemFacade.php @@ -0,0 +1,137 @@ + + */ + +namespace Whoops\Util; + +class SystemFacade +{ + /** + * Turns on output buffering. + * + * @return bool + */ + public function startOutputBuffering() + { + return ob_start(); + } + + /** + * @param callable $handler + * @param int $types + * + * @return callable|null + */ + public function setErrorHandler(callable $handler, $types = 'use-php-defaults') + { + // Workaround for PHP 5.5 + if ($types === 'use-php-defaults') { + $types = E_ALL | E_STRICT; + } + return set_error_handler($handler, $types); + } + + /** + * @param callable $handler + * + * @return callable|null + */ + public function setExceptionHandler(callable $handler) + { + return set_exception_handler($handler); + } + + /** + * @return void + */ + public function restoreExceptionHandler() + { + restore_exception_handler(); + } + + /** + * @return void + */ + public function restoreErrorHandler() + { + restore_error_handler(); + } + + /** + * @param callable $function + * + * @return void + */ + public function registerShutdownFunction(callable $function) + { + register_shutdown_function($function); + } + + /** + * @return string|false + */ + public function cleanOutputBuffer() + { + return ob_get_clean(); + } + + /** + * @return int + */ + public function getOutputBufferLevel() + { + return ob_get_level(); + } + + /** + * @return bool + */ + public function endOutputBuffering() + { + return ob_end_clean(); + } + + /** + * @return void + */ + public function flushOutputBuffer() + { + flush(); + } + + /** + * @return int + */ + public function getErrorReportingLevel() + { + return error_reporting(); + } + + /** + * @return array|null + */ + public function getLastError() + { + return error_get_last(); + } + + /** + * @param int $httpCode + * + * @return int + */ + public function setHttpResponseCode($httpCode) + { + return http_response_code($httpCode); + } + + /** + * @param int $exitStatus + */ + public function stopExecution($exitStatus) + { + exit($exitStatus); + } +} diff --git a/kirby/vendor/filp/whoops/src/Whoops/Util/TemplateHelper.php b/kirby/vendor/filp/whoops/src/Whoops/Util/TemplateHelper.php new file mode 100755 index 0000000..00f6ae4 --- /dev/null +++ b/kirby/vendor/filp/whoops/src/Whoops/Util/TemplateHelper.php @@ -0,0 +1,352 @@ + + */ + +namespace Whoops\Util; + +use Symfony\Component\VarDumper\Caster\Caster; +use Symfony\Component\VarDumper\Cloner\AbstractCloner; +use Symfony\Component\VarDumper\Cloner\VarCloner; +use Symfony\Component\VarDumper\Dumper\HtmlDumper; +use Whoops\Exception\Frame; + +/** + * Exposes useful tools for working with/in templates + */ +class TemplateHelper +{ + /** + * An array of variables to be passed to all templates + * @var array + */ + private $variables = []; + + /** + * @var HtmlDumper + */ + private $htmlDumper; + + /** + * @var HtmlDumperOutput + */ + private $htmlDumperOutput; + + /** + * @var AbstractCloner + */ + private $cloner; + + /** + * @var string + */ + private $applicationRootPath; + + public function __construct() + { + // root path for ordinary composer projects + $this->applicationRootPath = dirname(dirname(dirname(dirname(dirname(dirname(__DIR__)))))); + } + + /** + * Escapes a string for output in an HTML document + * + * @param string $raw + * @return string + */ + public function escape($raw) + { + $flags = ENT_QUOTES; + + // HHVM has all constants defined, but only ENT_IGNORE + // works at the moment + if (defined("ENT_SUBSTITUTE") && !defined("HHVM_VERSION")) { + $flags |= ENT_SUBSTITUTE; + } else { + // This is for 5.3. + // The documentation warns of a potential security issue, + // but it seems it does not apply in our case, because + // we do not blacklist anything anywhere. + $flags |= ENT_IGNORE; + } + + $raw = str_replace(chr(9), ' ', $raw); + + return htmlspecialchars($raw, $flags, "UTF-8"); + } + + /** + * Escapes a string for output in an HTML document, but preserves + * URIs within it, and converts them to clickable anchor elements. + * + * @param string $raw + * @return string + */ + public function escapeButPreserveUris($raw) + { + $escaped = $this->escape($raw); + return preg_replace( + "@([A-z]+?://([-\w\.]+[-\w])+(:\d+)?(/([\w/_\.#-]*(\?\S+)?[^\.\s])?)?)@", + "$1", + $escaped + ); + } + + /** + * Makes sure that the given string breaks on the delimiter. + * + * @param string $delimiter + * @param string $s + * @return string + */ + public function breakOnDelimiter($delimiter, $s) + { + $parts = explode($delimiter, $s); + foreach ($parts as &$part) { + $part = '
' . $part . '
'; + } + + return implode($delimiter, $parts); + } + + /** + * Replace the part of the path that all files have in common. + * + * @param string $path + * @return string + */ + public function shorten($path) + { + if ($this->applicationRootPath != "/") { + $path = str_replace($this->applicationRootPath, '…', $path); + } + + return $path; + } + + private function getDumper() + { + if (!$this->htmlDumper && class_exists('Symfony\Component\VarDumper\Cloner\VarCloner')) { + $this->htmlDumperOutput = new HtmlDumperOutput(); + // re-use the same var-dumper instance, so it won't re-render the global styles/scripts on each dump. + $this->htmlDumper = new HtmlDumper($this->htmlDumperOutput); + + $styles = [ + 'default' => 'color:#FFFFFF; line-height:normal; font:12px "Inconsolata", "Fira Mono", "Source Code Pro", Monaco, Consolas, "Lucida Console", monospace !important; word-wrap: break-word; white-space: pre-wrap; position:relative; z-index:99999; word-break: normal', + 'num' => 'color:#BCD42A', + 'const' => 'color: #4bb1b1;', + 'str' => 'color:#BCD42A', + 'note' => 'color:#ef7c61', + 'ref' => 'color:#A0A0A0', + 'public' => 'color:#FFFFFF', + 'protected' => 'color:#FFFFFF', + 'private' => 'color:#FFFFFF', + 'meta' => 'color:#FFFFFF', + 'key' => 'color:#BCD42A', + 'index' => 'color:#ef7c61', + ]; + $this->htmlDumper->setStyles($styles); + } + + return $this->htmlDumper; + } + + /** + * Format the given value into a human readable string. + * + * @param mixed $value + * @return string + */ + public function dump($value) + { + $dumper = $this->getDumper(); + + if ($dumper) { + // re-use the same DumpOutput instance, so it won't re-render the global styles/scripts on each dump. + // exclude verbose information (e.g. exception stack traces) + if (class_exists('Symfony\Component\VarDumper\Caster\Caster')) { + $cloneVar = $this->getCloner()->cloneVar($value, Caster::EXCLUDE_VERBOSE); + // Symfony VarDumper 2.6 Caster class dont exist. + } else { + $cloneVar = $this->getCloner()->cloneVar($value); + } + + $dumper->dump( + $cloneVar, + $this->htmlDumperOutput + ); + + $output = $this->htmlDumperOutput->getOutput(); + $this->htmlDumperOutput->clear(); + + return $output; + } + + return htmlspecialchars(print_r($value, true)); + } + + /** + * Format the args of the given Frame as a human readable html string + * + * @param Frame $frame + * @return string the rendered html + */ + public function dumpArgs(Frame $frame) + { + // we support frame args only when the optional dumper is available + if (!$this->getDumper()) { + return ''; + } + + $html = ''; + $numFrames = count($frame->getArgs()); + + if ($numFrames > 0) { + $html = '
    '; + foreach ($frame->getArgs() as $j => $frameArg) { + $html .= '
  1. '. $this->dump($frameArg) .'
  2. '; + } + $html .= '
'; + } + + return $html; + } + + /** + * Convert a string to a slug version of itself + * + * @param string $original + * @return string + */ + public function slug($original) + { + $slug = str_replace(" ", "-", $original); + $slug = preg_replace('/[^\w\d\-\_]/i', '', $slug); + return strtolower($slug); + } + + /** + * Given a template path, render it within its own scope. This + * method also accepts an array of additional variables to be + * passed to the template. + * + * @param string $template + * @param array $additionalVariables + */ + public function render($template, array $additionalVariables = null) + { + $variables = $this->getVariables(); + + // Pass the helper to the template: + $variables["tpl"] = $this; + + if ($additionalVariables !== null) { + $variables = array_replace($variables, $additionalVariables); + } + + call_user_func(function () { + extract(func_get_arg(1)); + require func_get_arg(0); + }, $template, $variables); + } + + /** + * Sets the variables to be passed to all templates rendered + * by this template helper. + * + * @param array $variables + */ + public function setVariables(array $variables) + { + $this->variables = $variables; + } + + /** + * Sets a single template variable, by its name: + * + * @param string $variableName + * @param mixed $variableValue + */ + public function setVariable($variableName, $variableValue) + { + $this->variables[$variableName] = $variableValue; + } + + /** + * Gets a single template variable, by its name, or + * $defaultValue if the variable does not exist + * + * @param string $variableName + * @param mixed $defaultValue + * @return mixed + */ + public function getVariable($variableName, $defaultValue = null) + { + return isset($this->variables[$variableName]) ? + $this->variables[$variableName] : $defaultValue; + } + + /** + * Unsets a single template variable, by its name + * + * @param string $variableName + */ + public function delVariable($variableName) + { + unset($this->variables[$variableName]); + } + + /** + * Returns all variables for this helper + * + * @return array + */ + public function getVariables() + { + return $this->variables; + } + + /** + * Set the cloner used for dumping variables. + * + * @param AbstractCloner $cloner + */ + public function setCloner($cloner) + { + $this->cloner = $cloner; + } + + /** + * Get the cloner used for dumping variables. + * + * @return AbstractCloner + */ + public function getCloner() + { + if (!$this->cloner) { + $this->cloner = new VarCloner(); + } + return $this->cloner; + } + + /** + * Set the application root path. + * + * @param string $applicationRootPath + */ + public function setApplicationRootPath($applicationRootPath) + { + $this->applicationRootPath = $applicationRootPath; + } + + /** + * Return the application root path. + * + * @return string + */ + public function getApplicationRootPath() + { + return $this->applicationRootPath; + } +} diff --git a/kirby/vendor/getkirby/composer-installer/src/ComposerInstaller/CmsInstaller.php b/kirby/vendor/getkirby/composer-installer/src/ComposerInstaller/CmsInstaller.php new file mode 100755 index 0000000..8a88ce4 --- /dev/null +++ b/kirby/vendor/getkirby/composer-installer/src/ComposerInstaller/CmsInstaller.php @@ -0,0 +1,64 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier + * @license https://opensource.org/licenses/MIT + */ +class CmsInstaller extends Installer +{ + /** + * Decides if the installer supports the given type + * + * @param string $packageType + * @return bool + */ + public function supports($packageType): bool + { + return $packageType === 'kirby-cms'; + } + + /** + * Returns the installation path of a package + * + * @param PackageInterface $package + * @return string path + */ + public function getInstallPath(PackageInterface $package): string + { + // get the extra configuration of the top-level package + if ($rootPackage = $this->composer->getPackage()) { + $extra = $rootPackage->getExtra(); + } else { + $extra = []; + } + + // use path from configuration, otherwise fall back to default + if (isset($extra['kirby-cms-path'])) { + $path = $extra['kirby-cms-path']; + } else { + $path = 'kirby'; + } + + // if explicitly set to something invalid (e.g. `false`), install to vendor dir + if (!is_string($path)) { + return parent::getInstallPath($package); + } + + // don't allow unsafe directories + $vendorDir = $this->composer->getConfig()->get('vendor-dir', Config::RELATIVE_PATHS) ?? 'vendor'; + if ($path === $vendorDir || $path === '.') { + throw new InvalidArgumentException('The path ' . $path . ' is an unsafe installation directory for ' . $package->getPrettyName() . '.'); + } + + return $path; + } +} diff --git a/kirby/vendor/getkirby/composer-installer/src/ComposerInstaller/Installer.php b/kirby/vendor/getkirby/composer-installer/src/ComposerInstaller/Installer.php new file mode 100755 index 0000000..48935fc --- /dev/null +++ b/kirby/vendor/getkirby/composer-installer/src/ComposerInstaller/Installer.php @@ -0,0 +1,80 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier + * @license https://opensource.org/licenses/MIT + */ +class Installer extends LibraryInstaller +{ + /** + * Decides if the installer supports the given type + * + * @param string $packageType + * @return bool + */ + public function supports($packageType): bool + { + throw new RuntimeException('This method needs to be overridden.'); // @codeCoverageIgnore + } + + /** + * Installs specific package. + * + * @param InstalledRepositoryInterface $repo repository in which to check + * @param PackageInterface $package package instance + */ + public function install(InstalledRepositoryInterface $repo, PackageInterface $package) + { + // first install the package normally... + parent::install($repo, $package); + + // ...then run custom code + $this->postInstall($package); + } + + /** + * Updates specific package. + * + * @param InstalledRepositoryInterface $repo repository in which to check + * @param PackageInterface $initial already installed package version + * @param PackageInterface $target updated version + * + * @throws InvalidArgumentException if $initial package is not installed + */ + public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) + { + // first update the package normally... + parent::update($repo, $initial, $target); + + // ...then run custom code + $this->postInstall($target); + } + + /** + * Custom handler that will be called after each package + * installation or update + * + * @param PackageInterface $package + */ + protected function postInstall(PackageInterface $package) + { + // remove the package's `vendor` directory to avoid duplicated autoloader and vendor code + $packageVendorDir = $this->getInstallPath($package) . '/vendor'; + if (is_dir($packageVendorDir)) { + $success = $this->filesystem->removeDirectory($packageVendorDir); + if (!$success) { + throw new RuntimeException('Could not completely delete ' . $path . ', aborting.'); // @codeCoverageIgnore + } + } + } +} diff --git a/kirby/vendor/getkirby/composer-installer/src/ComposerInstaller/Plugin.php b/kirby/vendor/getkirby/composer-installer/src/ComposerInstaller/Plugin.php new file mode 100755 index 0000000..4ead6d5 --- /dev/null +++ b/kirby/vendor/getkirby/composer-installer/src/ComposerInstaller/Plugin.php @@ -0,0 +1,30 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier + * @license https://opensource.org/licenses/MIT + */ +class Plugin implements PluginInterface +{ + /** + * Apply plugin modifications to Composer + * + * @param Composer $composer + * @param IOInterface $io + */ + public function activate(Composer $composer, IOInterface $io) + { + $installationManager = $composer->getInstallationManager(); + $installationManager->addInstaller(new CmsInstaller($io, $composer)); + $installationManager->addInstaller(new PluginInstaller($io, $composer)); + } +} diff --git a/kirby/vendor/getkirby/composer-installer/src/ComposerInstaller/PluginInstaller.php b/kirby/vendor/getkirby/composer-installer/src/ComposerInstaller/PluginInstaller.php new file mode 100755 index 0000000..1e56e13 --- /dev/null +++ b/kirby/vendor/getkirby/composer-installer/src/ComposerInstaller/PluginInstaller.php @@ -0,0 +1,102 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier + * @license https://opensource.org/licenses/MIT + */ +class PluginInstaller extends Installer +{ + /** + * Decides if the installer supports the given type + * + * @param string $packageType + * @return bool + */ + public function supports($packageType): bool + { + return $packageType === 'kirby-plugin'; + } + + /** + * Returns the installation path of a package + * + * @param PackageInterface $package + * @return string path + */ + public function getInstallPath(PackageInterface $package): string + { + // place into `vendor` directory as usual if Pluginkit is not supported + if ($this->supportsPluginkit($package) !== true) { + return parent::getInstallPath($package); + } + + // get the extra configuration of the top-level package + if ($rootPackage = $this->composer->getPackage()) { + $extra = $rootPackage->getExtra(); + } else { + $extra = []; + } + + // use base path from configuration, otherwise fall back to default + $basePath = $extra['kirby-plugin-path'] ?? 'site/plugins'; + + // determine the plugin name from its package name; + // can be overridden in the plugin's `composer.json` + $prettyName = $package->getPrettyName(); + $pluginExtra = $package->getExtra(); + if (!empty($pluginExtra['installer-name'])) { + $name = $pluginExtra['installer-name']; + } elseif (strpos($prettyName, '/') !== false) { + // use name after the slash + $name = explode('/', $prettyName)[1]; + } else { + $name = $prettyName; + } + + // build destination path from base path and plugin name + return $basePath . '/' . $name; + } + + /** + * Custom handler that will be called after each package + * installation or update + * + * @param PackageInterface $package + */ + protected function postInstall(PackageInterface $package) + { + // only continue if Pluginkit is supported + if ($this->supportsPluginkit($package) !== true) { + return; + } + + parent::postInstall($package); + } + + /** + * Checks if the package has explicitly required this installer; + * otherwise (if the Pluginkit is not yet supported by the plugin) + * the installer will fall back to the behavior of the LibraryInstaller + * + * @param PackageInterface $package + * @return bool + */ + protected function supportsPluginkit(PackageInterface $package): bool + { + foreach ($package->getRequires() as $link) { + if ($link->getTarget() === 'getkirby/composer-installer') { + return true; + } + } + + // no required package is the installer + return false; + } +} diff --git a/kirby/vendor/laminas/laminas-escaper/src/Escaper.php b/kirby/vendor/laminas/laminas-escaper/src/Escaper.php new file mode 100755 index 0000000..9f903a5 --- /dev/null +++ b/kirby/vendor/laminas/laminas-escaper/src/Escaper.php @@ -0,0 +1,391 @@ + 'quot', // quotation mark + 38 => 'amp', // ampersand + 60 => 'lt', // less-than sign + 62 => 'gt', // greater-than sign + ]; + + /** + * Current encoding for escaping. If not UTF-8, we convert strings from this encoding + * pre-escaping and back to this encoding post-escaping. + * + * @var string + */ + protected $encoding = 'utf-8'; + + /** + * Holds the value of the special flags passed as second parameter to + * htmlspecialchars(). + * + * @var int + */ + protected $htmlSpecialCharsFlags; + + /** + * Static Matcher which escapes characters for HTML Attribute contexts + * + * @var callable + */ + protected $htmlAttrMatcher; + + /** + * Static Matcher which escapes characters for Javascript contexts + * + * @var callable + */ + protected $jsMatcher; + + /** + * Static Matcher which escapes characters for CSS Attribute contexts + * + * @var callable + */ + protected $cssMatcher; + + /** + * List of all encoding supported by this class + * + * @var array + */ + protected $supportedEncodings = [ + 'iso-8859-1', 'iso8859-1', 'iso-8859-5', 'iso8859-5', + 'iso-8859-15', 'iso8859-15', 'utf-8', 'cp866', + 'ibm866', '866', 'cp1251', 'windows-1251', + 'win-1251', '1251', 'cp1252', 'windows-1252', + '1252', 'koi8-r', 'koi8-ru', 'koi8r', + 'big5', '950', 'gb2312', '936', + 'big5-hkscs', 'shift_jis', 'sjis', 'sjis-win', + 'cp932', '932', 'euc-jp', 'eucjp', + 'eucjp-win', 'macroman' + ]; + + /** + * Constructor: Single parameter allows setting of global encoding for use by + * the current object. + * + * @param string $encoding + * @throws Exception\InvalidArgumentException + */ + public function __construct($encoding = null) + { + if ($encoding !== null) { + if (! is_string($encoding)) { + throw new Exception\InvalidArgumentException( + get_class($this) . ' constructor parameter must be a string, received ' . gettype($encoding) + ); + } + if ($encoding === '') { + throw new Exception\InvalidArgumentException( + get_class($this) . ' constructor parameter does not allow a blank value' + ); + } + + $encoding = strtolower($encoding); + if (! in_array($encoding, $this->supportedEncodings)) { + throw new Exception\InvalidArgumentException( + 'Value of \'' . $encoding . '\' passed to ' . get_class($this) + . ' constructor parameter is invalid. Provide an encoding supported by htmlspecialchars()' + ); + } + + $this->encoding = $encoding; + } + + // We take advantage of ENT_SUBSTITUTE flag to correctly deal with invalid UTF-8 sequences. + $this->htmlSpecialCharsFlags = ENT_QUOTES | ENT_SUBSTITUTE; + + // set matcher callbacks + $this->htmlAttrMatcher = [$this, 'htmlAttrMatcher']; + $this->jsMatcher = [$this, 'jsMatcher']; + $this->cssMatcher = [$this, 'cssMatcher']; + } + + /** + * Return the encoding that all output/input is expected to be encoded in. + * + * @return string + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Escape a string for the HTML Body context where there are very few characters + * of special meaning. Internally this will use htmlspecialchars(). + * + * @param string $string + * @return string + */ + public function escapeHtml($string) + { + return htmlspecialchars($string, $this->htmlSpecialCharsFlags, $this->encoding); + } + + /** + * Escape a string for the HTML Attribute context. We use an extended set of characters + * to escape that are not covered by htmlspecialchars() to cover cases where an attribute + * might be unquoted or quoted illegally (e.g. backticks are valid quotes for IE). + * + * @param string $string + * @return string + */ + public function escapeHtmlAttr($string) + { + $string = $this->toUtf8($string); + if ($string === '' || ctype_digit($string)) { + return $string; + } + + $result = preg_replace_callback('/[^a-z0-9,\.\-_]/iSu', $this->htmlAttrMatcher, $string); + return $this->fromUtf8($result); + } + + /** + * Escape a string for the Javascript context. This does not use json_encode(). An extended + * set of characters are escaped beyond ECMAScript's rules for Javascript literal string + * escaping in order to prevent misinterpretation of Javascript as HTML leading to the + * injection of special characters and entities. The escaping used should be tolerant + * of cases where HTML escaping was not applied on top of Javascript escaping correctly. + * Backslash escaping is not used as it still leaves the escaped character as-is and so + * is not useful in a HTML context. + * + * @param string $string + * @return string + */ + public function escapeJs($string) + { + $string = $this->toUtf8($string); + if ($string === '' || ctype_digit($string)) { + return $string; + } + + $result = preg_replace_callback('/[^a-z0-9,\._]/iSu', $this->jsMatcher, $string); + return $this->fromUtf8($result); + } + + /** + * Escape a string for the URI or Parameter contexts. This should not be used to escape + * an entire URI - only a subcomponent being inserted. The function is a simple proxy + * to rawurlencode() which now implements RFC 3986 since PHP 5.3 completely. + * + * @param string $string + * @return string + */ + public function escapeUrl($string) + { + return rawurlencode($string); + } + + /** + * Escape a string for the CSS context. CSS escaping can be applied to any string being + * inserted into CSS and escapes everything except alphanumerics. + * + * @param string $string + * @return string + */ + public function escapeCss($string) + { + $string = $this->toUtf8($string); + if ($string === '' || ctype_digit($string)) { + return $string; + } + + $result = preg_replace_callback('/[^a-z0-9]/iSu', $this->cssMatcher, $string); + return $this->fromUtf8($result); + } + + /** + * Callback function for preg_replace_callback that applies HTML Attribute + * escaping to all matches. + * + * @param array $matches + * @return string + */ + protected function htmlAttrMatcher($matches) + { + $chr = $matches[0]; + $ord = ord($chr); + + /** + * The following replaces characters undefined in HTML with the + * hex entity for the Unicode replacement character. + */ + if (($ord <= 0x1f && $chr != "\t" && $chr != "\n" && $chr != "\r") + || ($ord >= 0x7f && $ord <= 0x9f) + ) { + return '�'; + } + + /** + * Check if the current character to escape has a name entity we should + * replace it with while grabbing the integer value of the character. + */ + if (strlen($chr) > 1) { + $chr = $this->convertEncoding($chr, 'UTF-32BE', 'UTF-8'); + } + + $hex = bin2hex($chr); + $ord = hexdec($hex); + if (isset(static::$htmlNamedEntityMap[$ord])) { + return '&' . static::$htmlNamedEntityMap[$ord] . ';'; + } + + /** + * Per OWASP recommendations, we'll use upper hex entities + * for any other characters where a named entity does not exist. + */ + if ($ord > 255) { + return sprintf('&#x%04X;', $ord); + } + return sprintf('&#x%02X;', $ord); + } + + /** + * Callback function for preg_replace_callback that applies Javascript + * escaping to all matches. + * + * @param array $matches + * @return string + */ + protected function jsMatcher($matches) + { + $chr = $matches[0]; + if (strlen($chr) == 1) { + return sprintf('\\x%02X', ord($chr)); + } + $chr = $this->convertEncoding($chr, 'UTF-16BE', 'UTF-8'); + $hex = strtoupper(bin2hex($chr)); + if (strlen($hex) <= 4) { + return sprintf('\\u%04s', $hex); + } + $highSurrogate = substr($hex, 0, 4); + $lowSurrogate = substr($hex, 4, 4); + return sprintf('\\u%04s\\u%04s', $highSurrogate, $lowSurrogate); + } + + /** + * Callback function for preg_replace_callback that applies CSS + * escaping to all matches. + * + * @param array $matches + * @return string + */ + protected function cssMatcher($matches) + { + $chr = $matches[0]; + if (strlen($chr) == 1) { + $ord = ord($chr); + } else { + $chr = $this->convertEncoding($chr, 'UTF-32BE', 'UTF-8'); + $ord = hexdec(bin2hex($chr)); + } + return sprintf('\\%X ', $ord); + } + + /** + * Converts a string to UTF-8 from the base encoding. The base encoding is set via this + * class' constructor. + * + * @param string $string + * @throws Exception\RuntimeException + * @return string + */ + protected function toUtf8($string) + { + if ($this->getEncoding() === 'utf-8') { + $result = $string; + } else { + $result = $this->convertEncoding($string, 'UTF-8', $this->getEncoding()); + } + + if (! $this->isUtf8($result)) { + throw new Exception\RuntimeException( + sprintf('String to be escaped was not valid UTF-8 or could not be converted: %s', $result) + ); + } + + return $result; + } + + /** + * Converts a string from UTF-8 to the base encoding. The base encoding is set via this + * class' constructor. + * @param string $string + * @return string + */ + protected function fromUtf8($string) + { + if ($this->getEncoding() === 'utf-8') { + return $string; + } + + return $this->convertEncoding($string, $this->getEncoding(), 'UTF-8'); + } + + /** + * Checks if a given string appears to be valid UTF-8 or not. + * + * @param string $string + * @return bool + */ + protected function isUtf8($string) + { + return ($string === '' || preg_match('/^./su', $string)); + } + + /** + * Encoding conversion helper which wraps iconv and mbstring where they exist or throws + * and exception where neither is available. + * + * @param string $string + * @param string $to + * @param array|string $from + * @throws Exception\RuntimeException + * @return string + */ + protected function convertEncoding($string, $to, $from) + { + if (function_exists('iconv')) { + $result = iconv($from, $to, $string); + } elseif (function_exists('mb_convert_encoding')) { + $result = mb_convert_encoding($string, $to, $from); + } else { + throw new Exception\RuntimeException( + get_class($this) + . ' requires either the iconv or mbstring extension to be installed' + . ' when escaping for non UTF-8 strings.' + ); + } + + if ($result === false) { + return ''; // return non-fatal blank string on encoding errors from users + } + return $result; + } +} diff --git a/kirby/vendor/laminas/laminas-escaper/src/Exception/ExceptionInterface.php b/kirby/vendor/laminas/laminas-escaper/src/Exception/ExceptionInterface.php new file mode 100755 index 0000000..7ebe04e --- /dev/null +++ b/kirby/vendor/laminas/laminas-escaper/src/Exception/ExceptionInterface.php @@ -0,0 +1,13 @@ + 'zendframework/zendframework', + 'zend-developer-tools/toolbar/doctrine' => 'zend-developer-tools/toolbar/doctrine', + + // NAMESPACES + // Zend Framework components + 'Zend\\AuraDi\\Config' => 'Laminas\\AuraDi\\Config', + 'Zend\\Authentication' => 'Laminas\\Authentication', + 'Zend\\Barcode' => 'Laminas\\Barcode', + 'Zend\\Cache' => 'Laminas\\Cache', + 'Zend\\Captcha' => 'Laminas\\Captcha', + 'Zend\\Code' => 'Laminas\\Code', + 'ZendCodingStandard\\Sniffs' => 'LaminasCodingStandard\\Sniffs', + 'ZendCodingStandard\\Utils' => 'LaminasCodingStandard\\Utils', + 'Zend\\ComponentInstaller' => 'Laminas\\ComponentInstaller', + 'Zend\\Config' => 'Laminas\\Config', + 'Zend\\ConfigAggregator' => 'Laminas\\ConfigAggregator', + 'Zend\\ConfigAggregatorModuleManager' => 'Laminas\\ConfigAggregatorModuleManager', + 'Zend\\ConfigAggregatorParameters' => 'Laminas\\ConfigAggregatorParameters', + 'Zend\\Console' => 'Laminas\\Console', + 'Zend\\ContainerConfigTest' => 'Laminas\\ContainerConfigTest', + 'Zend\\Crypt' => 'Laminas\\Crypt', + 'Zend\\Db' => 'Laminas\\Db', + 'ZendDeveloperTools' => 'Laminas\\DeveloperTools', + 'Zend\\Di' => 'Laminas\\Di', + 'Zend\\Diactoros' => 'Laminas\\Diactoros', + 'ZendDiagnostics\\Check' => 'Laminas\\Diagnostics\\Check', + 'ZendDiagnostics\\Result' => 'Laminas\\Diagnostics\\Result', + 'ZendDiagnostics\\Runner' => 'Laminas\\Diagnostics\\Runner', + 'Zend\\Dom' => 'Laminas\\Dom', + 'Zend\\Escaper' => 'Laminas\\Escaper', + 'Zend\\EventManager' => 'Laminas\\EventManager', + 'Zend\\Feed' => 'Laminas\\Feed', + 'Zend\\File' => 'Laminas\\File', + 'Zend\\Filter' => 'Laminas\\Filter', + 'Zend\\Form' => 'Laminas\\Form', + 'Zend\\Http' => 'Laminas\\Http', + 'Zend\\HttpHandlerRunner' => 'Laminas\\HttpHandlerRunner', + 'Zend\\Hydrator' => 'Laminas\\Hydrator', + 'Zend\\I18n' => 'Laminas\\I18n', + 'Zend\\InputFilter' => 'Laminas\\InputFilter', + 'Zend\\Json' => 'Laminas\\Json', + 'Zend\\Ldap' => 'Laminas\\Ldap', + 'Zend\\Loader' => 'Laminas\\Loader', + 'Zend\\Log' => 'Laminas\\Log', + 'Zend\\Mail' => 'Laminas\\Mail', + 'Zend\\Math' => 'Laminas\\Math', + 'Zend\\Memory' => 'Laminas\\Memory', + 'Zend\\Mime' => 'Laminas\\Mime', + 'Zend\\ModuleManager' => 'Laminas\\ModuleManager', + 'Zend\\Mvc' => 'Laminas\\Mvc', + 'Zend\\Navigation' => 'Laminas\\Navigation', + 'Zend\\Paginator' => 'Laminas\\Paginator', + 'Zend\\Permissions' => 'Laminas\\Permissions', + 'Zend\\Pimple\\Config' => 'Laminas\\Pimple\\Config', + 'Zend\\ProblemDetails' => 'Mezzio\\ProblemDetails', + 'Zend\\ProgressBar' => 'Laminas\\ProgressBar', + 'Zend\\Psr7Bridge' => 'Laminas\\Psr7Bridge', + 'Zend\\Router' => 'Laminas\\Router', + 'Zend\\Serializer' => 'Laminas\\Serializer', + 'Zend\\Server' => 'Laminas\\Server', + 'Zend\\ServiceManager' => 'Laminas\\ServiceManager', + 'ZendService\\ReCaptcha' => 'Laminas\\ReCaptcha', + 'ZendService\\Twitter' => 'Laminas\\Twitter', + 'Zend\\Session' => 'Laminas\\Session', + 'Zend\\SkeletonInstaller' => 'Laminas\\SkeletonInstaller', + 'Zend\\Soap' => 'Laminas\\Soap', + 'Zend\\Stdlib' => 'Laminas\\Stdlib', + 'Zend\\Stratigility' => 'Laminas\\Stratigility', + 'Zend\\Tag' => 'Laminas\\Tag', + 'Zend\\Test' => 'Laminas\\Test', + 'Zend\\Text' => 'Laminas\\Text', + 'Zend\\Uri' => 'Laminas\\Uri', + 'Zend\\Validator' => 'Laminas\\Validator', + 'Zend\\View' => 'Laminas\\View', + 'ZendXml' => 'Laminas\\Xml', + 'Zend\\Xml2Json' => 'Laminas\\Xml2Json', + 'Zend\\XmlRpc' => 'Laminas\\XmlRpc', + 'ZendOAuth' => 'Laminas\\OAuth', + + 'Zend\\\\AuraDi\\\\Config' => 'Laminas\\\\AuraDi\\\\Config', + 'Zend\\\\Authentication' => 'Laminas\\\\Authentication', + 'Zend\\\\Barcode' => 'Laminas\\\\Barcode', + 'Zend\\\\Cache' => 'Laminas\\\\Cache', + 'Zend\\\\Captcha' => 'Laminas\\\\Captcha', + 'Zend\\\\Code' => 'Laminas\\\\Code', + 'ZendCodingStandard\\\\Sniffs' => 'LaminasCodingStandard\\\\Sniffs', + 'ZendCodingStandard\\\\Utils' => 'LaminasCodingStandard\\\\Utils', + 'Zend\\\\ComponentInstaller' => 'Laminas\\\\ComponentInstaller', + 'Zend\\\\Config' => 'Laminas\\\\Config', + 'Zend\\\\ConfigAggregator' => 'Laminas\\\\ConfigAggregator', + 'Zend\\\\ConfigAggregatorModuleManager' => 'Laminas\\\\ConfigAggregatorModuleManager', + 'Zend\\\\ConfigAggregatorParameters' => 'Laminas\\\\ConfigAggregatorParameters', + 'Zend\\\\Console' => 'Laminas\\\\Console', + 'Zend\\\\ContainerConfigTest' => 'Laminas\\\\ContainerConfigTest', + 'Zend\\\\Crypt' => 'Laminas\\\\Crypt', + 'Zend\\\\Db' => 'Laminas\\\\Db', + 'Zend\\\\Di' => 'Laminas\\\\Di', + 'Zend\\\\Diactoros' => 'Laminas\\\\Diactoros', + 'ZendDiagnostics\\\\Check' => 'Laminas\\\\Diagnostics\\\\Check', + 'ZendDiagnostics\\\\Result' => 'Laminas\\\\Diagnostics\\\\Result', + 'ZendDiagnostics\\\\Runner' => 'Laminas\\\\Diagnostics\\\\Runner', + 'Zend\\\\Dom' => 'Laminas\\\\Dom', + 'Zend\\\\Escaper' => 'Laminas\\\\Escaper', + 'Zend\\\\EventManager' => 'Laminas\\\\EventManager', + 'Zend\\\\Feed' => 'Laminas\\\\Feed', + 'Zend\\\\File' => 'Laminas\\\\File', + 'Zend\\\\Filter' => 'Laminas\\\\Filter', + 'Zend\\\\Form' => 'Laminas\\\\Form', + 'Zend\\\\Http' => 'Laminas\\\\Http', + 'Zend\\\\HttpHandlerRunner' => 'Laminas\\\\HttpHandlerRunner', + 'Zend\\\\Hydrator' => 'Laminas\\\\Hydrator', + 'Zend\\\\I18n' => 'Laminas\\\\I18n', + 'Zend\\\\InputFilter' => 'Laminas\\\\InputFilter', + 'Zend\\\\Json' => 'Laminas\\\\Json', + 'Zend\\\\Ldap' => 'Laminas\\\\Ldap', + 'Zend\\\\Loader' => 'Laminas\\\\Loader', + 'Zend\\\\Log' => 'Laminas\\\\Log', + 'Zend\\\\Mail' => 'Laminas\\\\Mail', + 'Zend\\\\Math' => 'Laminas\\\\Math', + 'Zend\\\\Memory' => 'Laminas\\\\Memory', + 'Zend\\\\Mime' => 'Laminas\\\\Mime', + 'Zend\\\\ModuleManager' => 'Laminas\\\\ModuleManager', + 'Zend\\\\Mvc' => 'Laminas\\\\Mvc', + 'Zend\\\\Navigation' => 'Laminas\\\\Navigation', + 'Zend\\\\Paginator' => 'Laminas\\\\Paginator', + 'Zend\\\\Permissions' => 'Laminas\\\\Permissions', + 'Zend\\\\Pimple\\\\Config' => 'Laminas\\\\Pimple\\\\Config', + 'Zend\\\\ProblemDetails' => 'Mezzio\\\\ProblemDetails', + 'Zend\\\\ProgressBar' => 'Laminas\\\\ProgressBar', + 'Zend\\\\Psr7Bridge' => 'Laminas\\\\Psr7Bridge', + 'Zend\\\\Router' => 'Laminas\\\\Router', + 'Zend\\\\Serializer' => 'Laminas\\\\Serializer', + 'Zend\\\\Server' => 'Laminas\\\\Server', + 'Zend\\\\ServiceManager' => 'Laminas\\\\ServiceManager', + 'ZendService\\\\ReCaptcha' => 'Laminas\\\\ReCaptcha', + 'ZendService\\\\Twitter' => 'Laminas\\\\Twitter', + 'Zend\\\\Session' => 'Laminas\\\\Session', + 'Zend\\\\SkeletonInstaller' => 'Laminas\\\\SkeletonInstaller', + 'Zend\\\\Soap' => 'Laminas\\\\Soap', + 'Zend\\\\Stdlib' => 'Laminas\\\\Stdlib', + 'Zend\\\\Stratigility' => 'Laminas\\\\Stratigility', + 'Zend\\\\Tag' => 'Laminas\\\\Tag', + 'Zend\\\\Test' => 'Laminas\\\\Test', + 'Zend\\\\Text' => 'Laminas\\\\Text', + 'Zend\\\\Uri' => 'Laminas\\\\Uri', + 'Zend\\\\Validator' => 'Laminas\\\\Validator', + 'Zend\\\\View' => 'Laminas\\\\View', + 'Zend\\\\Xml2Json' => 'Laminas\\\\Xml2Json', + 'Zend\\\\XmlRpc' => 'Laminas\\\\XmlRpc', + 'ZendHttp' => 'LaminasHttp', // class ZendHttpClientDecorator in zend-feed + 'ZendModule' => 'LaminasModule', // class ZendModuleProvider in zend-config-aggregator-modulemanager + 'a\\Zend\\' => 'a\\Zend\\', + 'b\\Zend\\' => 'b\\Zend\\', + 'c\\Zend\\' => 'c\\Zend\\', + 'd\\Zend\\' => 'd\\Zend\\', + 'e\\Zend\\' => 'e\\Zend\\', + 'f\\Zend\\' => 'f\\Zend\\', + 'g\\Zend\\' => 'g\\Zend\\', + 'h\\Zend\\' => 'h\\Zend\\', + 'i\\Zend\\' => 'i\\Zend\\', + 'j\\Zend\\' => 'j\\Zend\\', + 'k\\Zend\\' => 'k\\Zend\\', + 'l\\Zend\\' => 'l\\Zend\\', + 'm\\Zend\\' => 'm\\Zend\\', + 'n\\Zend\\' => 'n\\Zend\\', + 'o\\Zend\\' => 'o\\Zend\\', + 'p\\Zend\\' => 'p\\Zend\\', + 'q\\Zend\\' => 'q\\Zend\\', + 'r\\Zend\\' => 'r\\Zend\\', + 's\\Zend\\' => 's\\Zend\\', + 't\\Zend\\' => 't\\Zend\\', + 'u\\Zend\\' => 'u\\Zend\\', + 'v\\Zend\\' => 'v\\Zend\\', + 'w\\Zend\\' => 'w\\Zend\\', + 'x\\Zend\\' => 'x\\Zend\\', + 'y\\Zend\\' => 'y\\Zend\\', + 'z\\Zend\\' => 'z\\Zend\\', + 'a\\ZendOAuth\\' => 'a\\ZendOAuth\\', + 'b\\ZendOAuth\\' => 'b\\ZendOAuth\\', + 'c\\ZendOAuth\\' => 'c\\ZendOAuth\\', + 'd\\ZendOAuth\\' => 'd\\ZendOAuth\\', + 'e\\ZendOAuth\\' => 'e\\ZendOAuth\\', + 'f\\ZendOAuth\\' => 'f\\ZendOAuth\\', + 'g\\ZendOAuth\\' => 'g\\ZendOAuth\\', + 'h\\ZendOAuth\\' => 'h\\ZendOAuth\\', + 'i\\ZendOAuth\\' => 'i\\ZendOAuth\\', + 'j\\ZendOAuth\\' => 'j\\ZendOAuth\\', + 'k\\ZendOAuth\\' => 'k\\ZendOAuth\\', + 'l\\ZendOAuth\\' => 'l\\ZendOAuth\\', + 'm\\ZendOAuth\\' => 'm\\ZendOAuth\\', + 'n\\ZendOAuth\\' => 'n\\ZendOAuth\\', + 'o\\ZendOAuth\\' => 'o\\ZendOAuth\\', + 'p\\ZendOAuth\\' => 'p\\ZendOAuth\\', + 'q\\ZendOAuth\\' => 'q\\ZendOAuth\\', + 'r\\ZendOAuth\\' => 'r\\ZendOAuth\\', + 's\\ZendOAuth\\' => 's\\ZendOAuth\\', + 't\\ZendOAuth\\' => 't\\ZendOAuth\\', + 'u\\ZendOAuth\\' => 'u\\ZendOAuth\\', + 'v\\ZendOAuth\\' => 'v\\ZendOAuth\\', + 'w\\ZendOAuth\\' => 'w\\ZendOAuth\\', + 'x\\ZendOAuth\\' => 'x\\ZendOAuth\\', + 'y\\ZendOAuth\\' => 'y\\ZendOAuth\\', + 'z\\ZendOAuth\\' => 'z\\ZendOAuth\\', + 'a\\ZendService\\' => 'a\\ZendService\\', + 'b\\ZendService\\' => 'b\\ZendService\\', + 'c\\ZendService\\' => 'c\\ZendService\\', + 'd\\ZendService\\' => 'd\\ZendService\\', + 'e\\ZendService\\' => 'e\\ZendService\\', + 'f\\ZendService\\' => 'f\\ZendService\\', + 'g\\ZendService\\' => 'g\\ZendService\\', + 'h\\ZendService\\' => 'h\\ZendService\\', + 'i\\ZendService\\' => 'i\\ZendService\\', + 'j\\ZendService\\' => 'j\\ZendService\\', + 'k\\ZendService\\' => 'k\\ZendService\\', + 'l\\ZendService\\' => 'l\\ZendService\\', + 'm\\ZendService\\' => 'm\\ZendService\\', + 'n\\ZendService\\' => 'n\\ZendService\\', + 'o\\ZendService\\' => 'o\\ZendService\\', + 'p\\ZendService\\' => 'p\\ZendService\\', + 'q\\ZendService\\' => 'q\\ZendService\\', + 'r\\ZendService\\' => 'r\\ZendService\\', + 's\\ZendService\\' => 's\\ZendService\\', + 't\\ZendService\\' => 't\\ZendService\\', + 'u\\ZendService\\' => 'u\\ZendService\\', + 'v\\ZendService\\' => 'v\\ZendService\\', + 'w\\ZendService\\' => 'w\\ZendService\\', + 'x\\ZendService\\' => 'x\\ZendService\\', + 'y\\ZendService\\' => 'y\\ZendService\\', + 'z\\ZendService\\' => 'z\\ZendService\\', + 'a\\ZendXml\\' => 'a\\ZendXml\\', + 'b\\ZendXml\\' => 'b\\ZendXml\\', + 'c\\ZendXml\\' => 'c\\ZendXml\\', + 'd\\ZendXml\\' => 'd\\ZendXml\\', + 'e\\ZendXml\\' => 'e\\ZendXml\\', + 'f\\ZendXml\\' => 'f\\ZendXml\\', + 'g\\ZendXml\\' => 'g\\ZendXml\\', + 'h\\ZendXml\\' => 'h\\ZendXml\\', + 'i\\ZendXml\\' => 'i\\ZendXml\\', + 'j\\ZendXml\\' => 'j\\ZendXml\\', + 'k\\ZendXml\\' => 'k\\ZendXml\\', + 'l\\ZendXml\\' => 'l\\ZendXml\\', + 'm\\ZendXml\\' => 'm\\ZendXml\\', + 'n\\ZendXml\\' => 'n\\ZendXml\\', + 'o\\ZendXml\\' => 'o\\ZendXml\\', + 'p\\ZendXml\\' => 'p\\ZendXml\\', + 'q\\ZendXml\\' => 'q\\ZendXml\\', + 'r\\ZendXml\\' => 'r\\ZendXml\\', + 's\\ZendXml\\' => 's\\ZendXml\\', + 't\\ZendXml\\' => 't\\ZendXml\\', + 'u\\ZendXml\\' => 'u\\ZendXml\\', + 'v\\ZendXml\\' => 'v\\ZendXml\\', + 'w\\ZendXml\\' => 'w\\ZendXml\\', + 'x\\ZendXml\\' => 'x\\ZendXml\\', + 'y\\ZendXml\\' => 'y\\ZendXml\\', + 'z\\ZendXml\\' => 'z\\ZendXml\\', + + // Expressive + 'Zend\\Expressive' => 'Mezzio', + 'Zend\\\\Expressive' => 'Mezzio', + 'ZendAuthentication' => 'LaminasAuthentication', + 'ZendAcl' => 'LaminasAcl', + 'ZendRbac' => 'LaminasRbac', + 'ZendRouter' => 'LaminasRouter', + 'ExpressiveUrlGenerator' => 'MezzioUrlGenerator', + 'ExpressiveInstaller' => 'MezzioInstaller', + + // Apigility + 'ZF\\Apigility' => 'Laminas\\ApiTools', + 'ZF\\ApiProblem' => 'Laminas\\ApiTools\\ApiProblem', + 'ZF\\AssetManager' => 'Laminas\\ApiTools\\AssetManager', + 'ZF\\ComposerAutoloading' => 'Laminas\\ComposerAutoloading', + 'ZF\\Configuration' => 'Laminas\\ApiTools\\Configuration', + 'ZF\\ContentNegotiation' => 'Laminas\\ApiTools\\ContentNegotiation', + 'ZF\\ContentValidation' => 'Laminas\\ApiTools\\ContentValidation', + 'ZF\\DevelopmentMode' => 'Laminas\\DevelopmentMode', + 'ZF\\Doctrine\\QueryBuilder' => 'Laminas\\ApiTools\\Doctrine\\QueryBuilder', + 'ZF\\Hal' => 'Laminas\\ApiTools\\Hal', + 'ZF\\HttpCache' => 'Laminas\\ApiTools\\HttpCache', + 'ZF\\MvcAuth' => 'Laminas\\ApiTools\\MvcAuth', + 'ZF\\OAuth2' => 'Laminas\\ApiTools\\OAuth2', + 'ZF\\Rest' => 'Laminas\\ApiTools\\Rest', + 'ZF\\Rpc' => 'Laminas\\ApiTools\\Rpc', + 'ZF\\Versioning' => 'Laminas\\ApiTools\\Versioning', + 'a\\ZF\\' => 'a\\ZF\\', + 'b\\ZF\\' => 'b\\ZF\\', + 'c\\ZF\\' => 'c\\ZF\\', + 'd\\ZF\\' => 'd\\ZF\\', + 'e\\ZF\\' => 'e\\ZF\\', + 'f\\ZF\\' => 'f\\ZF\\', + 'g\\ZF\\' => 'g\\ZF\\', + 'h\\ZF\\' => 'h\\ZF\\', + 'i\\ZF\\' => 'i\\ZF\\', + 'j\\ZF\\' => 'j\\ZF\\', + 'k\\ZF\\' => 'k\\ZF\\', + 'l\\ZF\\' => 'l\\ZF\\', + 'm\\ZF\\' => 'm\\ZF\\', + 'n\\ZF\\' => 'n\\ZF\\', + 'o\\ZF\\' => 'o\\ZF\\', + 'p\\ZF\\' => 'p\\ZF\\', + 'q\\ZF\\' => 'q\\ZF\\', + 'r\\ZF\\' => 'r\\ZF\\', + 's\\ZF\\' => 's\\ZF\\', + 't\\ZF\\' => 't\\ZF\\', + 'u\\ZF\\' => 'u\\ZF\\', + 'v\\ZF\\' => 'v\\ZF\\', + 'w\\ZF\\' => 'w\\ZF\\', + 'x\\ZF\\' => 'x\\ZF\\', + 'y\\ZF\\' => 'y\\ZF\\', + 'z\\ZF\\' => 'z\\ZF\\', + + 'ZF\\\\Apigility' => 'Laminas\\\\ApiTools', + 'ZF\\\\ApiProblem' => 'Laminas\\\\ApiTools\\\\ApiProblem', + 'ZF\\\\AssetManager' => 'Laminas\\\\ApiTools\\\\AssetManager', + 'ZF\\\\ComposerAutoloading' => 'Laminas\\\\ComposerAutoloading', + 'ZF\\\\Configuration' => 'Laminas\\\\ApiTools\\\\Configuration', + 'ZF\\\\ContentNegotiation' => 'Laminas\\\\ApiTools\\\\ContentNegotiation', + 'ZF\\\\ContentValidation' => 'Laminas\\\\ApiTools\\\\ContentValidation', + 'ZF\\\\DevelopmentMode' => 'Laminas\\\\DevelopmentMode', + 'ZF\\\\Doctrine\\\\QueryBuilder' => 'Laminas\\\\ApiTools\\\\Doctrine\\\\QueryBuilder', + 'ZF\\\\Hal' => 'Laminas\\\\ApiTools\\\\Hal', + 'ZF\\\\HttpCache' => 'Laminas\\\\ApiTools\\\\HttpCache', + 'ZF\\\\MvcAuth' => 'Laminas\\\\ApiTools\\\\MvcAuth', + 'ZF\\\\OAuth2' => 'Laminas\\\\ApiTools\\\\OAuth2', + 'ZF\\\\Rest' => 'Laminas\\\\ApiTools\\\\Rest', + 'ZF\\\\Rpc' => 'Laminas\\\\ApiTools\\\\Rpc', + 'ZF\\\\Versioning' => 'Laminas\\\\ApiTools\\\\Versioning', + 'ApigilityModuleInterface' => 'ApiToolsModuleInterface', + 'ApigilityProviderInterface' => 'ApiToolsProviderInterface', + 'ApigilityVersionController' => 'ApiToolsVersionController', + + // PACKAGES + // ZF components, MVC + 'zendframework/skeleton-application' => 'laminas/skeleton-application', + 'zendframework/zend-auradi-config' => 'laminas/laminas-auradi-config', + 'zendframework/zend-authentication' => 'laminas/laminas-authentication', + 'zendframework/zend-barcode' => 'laminas/laminas-barcode', + 'zendframework/zend-cache' => 'laminas/laminas-cache', + 'zendframework/zend-captcha' => 'laminas/laminas-captcha', + 'zendframework/zend-code' => 'laminas/laminas-code', + 'zendframework/zend-coding-standard' => 'laminas/laminas-coding-standard', + 'zendframework/zend-component-installer' => 'laminas/laminas-component-installer', + 'zendframework/zend-composer-autoloading' => 'laminas/laminas-composer-autoloading', + 'zendframework/zend-config-aggregator' => 'laminas/laminas-config-aggregator', + 'zendframework/zend-config' => 'laminas/laminas-config', + 'zendframework/zend-console' => 'laminas/laminas-console', + 'zendframework/zend-container-config-test' => 'laminas/laminas-container-config-test', + 'zendframework/zend-crypt' => 'laminas/laminas-crypt', + 'zendframework/zend-db' => 'laminas/laminas-db', + 'zendframework/zend-developer-tools' => 'laminas/laminas-developer-tools', + 'zendframework/zend-diactoros' => 'laminas/laminas-diactoros', + 'zendframework/zenddiagnostics' => 'laminas/laminas-diagnostics', + 'zendframework/zend-di' => 'laminas/laminas-di', + 'zendframework/zend-dom' => 'laminas/laminas-dom', + 'zendframework/zend-escaper' => 'laminas/laminas-escaper', + 'zendframework/zend-eventmanager' => 'laminas/laminas-eventmanager', + 'zendframework/zend-feed' => 'laminas/laminas-feed', + 'zendframework/zend-file' => 'laminas/laminas-file', + 'zendframework/zend-filter' => 'laminas/laminas-filter', + 'zendframework/zend-form' => 'laminas/laminas-form', + 'zendframework/zend-httphandlerrunner' => 'laminas/laminas-httphandlerrunner', + 'zendframework/zend-http' => 'laminas/laminas-http', + 'zendframework/zend-hydrator' => 'laminas/laminas-hydrator', + 'zendframework/zend-i18n' => 'laminas/laminas-i18n', + 'zendframework/zend-i18n-resources' => 'laminas/laminas-i18n-resources', + 'zendframework/zend-inputfilter' => 'laminas/laminas-inputfilter', + 'zendframework/zend-json' => 'laminas/laminas-json', + 'zendframework/zend-json-server' => 'laminas/laminas-json-server', + 'zendframework/zend-ldap' => 'laminas/laminas-ldap', + 'zendframework/zend-loader' => 'laminas/laminas-loader', + 'zendframework/zend-log' => 'laminas/laminas-log', + 'zendframework/zend-mail' => 'laminas/laminas-mail', + 'zendframework/zend-math' => 'laminas/laminas-math', + 'zendframework/zend-memory' => 'laminas/laminas-memory', + 'zendframework/zend-mime' => 'laminas/laminas-mime', + 'zendframework/zend-modulemanager' => 'laminas/laminas-modulemanager', + 'zendframework/zend-mvc' => 'laminas/laminas-mvc', + 'zendframework/zend-navigation' => 'laminas/laminas-navigation', + 'zendframework/zend-oauth' => 'laminas/laminas-oauth', + 'zendframework/zend-paginator' => 'laminas/laminas-paginator', + 'zendframework/zend-permissions-acl' => 'laminas/laminas-permissions-acl', + 'zendframework/zend-permissions-rbac' => 'laminas/laminas-permissions-rbac', + 'zendframework/zend-pimple-config' => 'laminas/laminas-pimple-config', + 'zendframework/zend-progressbar' => 'laminas/laminas-progressbar', + 'zendframework/zend-psr7bridge' => 'laminas/laminas-psr7bridge', + 'zendframework/zend-recaptcha' => 'laminas/laminas-recaptcha', + 'zendframework/zend-router' => 'laminas/laminas-router', + 'zendframework/zend-serializer' => 'laminas/laminas-serializer', + 'zendframework/zend-server' => 'laminas/laminas-server', + 'zendframework/zend-servicemanager' => 'laminas/laminas-servicemanager', + 'zendframework/zendservice-recaptcha' => 'laminas/laminas-recaptcha', + 'zendframework/zendservice-twitter' => 'laminas/laminas-twitter', + 'zendframework/zend-session' => 'laminas/laminas-session', + 'zendframework/zend-skeleton-installer' => 'laminas/laminas-skeleton-installer', + 'zendframework/zend-soap' => 'laminas/laminas-soap', + 'zendframework/zend-stdlib' => 'laminas/laminas-stdlib', + 'zendframework/zend-stratigility' => 'laminas/laminas-stratigility', + 'zendframework/zend-tag' => 'laminas/laminas-tag', + 'zendframework/zend-test' => 'laminas/laminas-test', + 'zendframework/zend-text' => 'laminas/laminas-text', + 'zendframework/zend-uri' => 'laminas/laminas-uri', + 'zendframework/zend-validator' => 'laminas/laminas-validator', + 'zendframework/zend-view' => 'laminas/laminas-view', + 'zendframework/zend-xml2json' => 'laminas/laminas-xml2json', + 'zendframework/zend-xml' => 'laminas/laminas-xml', + 'zendframework/zend-xmlrpc' => 'laminas/laminas-xmlrpc', + + // Expressive packages + 'zendframework/zend-expressive' => 'mezzio/mezzio', + 'zendframework/zend-expressive-zendrouter' => 'mezzio/mezzio-laminasrouter', + 'zendframework/zend-problem-details' => 'mezzio/mezzio-problem-details', + 'zendframework/zend-expressive-zendviewrenderer' => 'mezzio/mezzio-laminasviewrenderer', + + // Apigility packages + 'zfcampus/apigility-documentation' => 'laminas-api-tools/documentation', + 'zfcampus/statuslib-example' => 'laminas-api-tools/statuslib-example', + 'zfcampus/zf-apigility' => 'laminas-api-tools/api-tools', + 'zfcampus/zf-api-problem' => 'laminas-api-tools/api-tools-api-problem', + 'zfcampus/zf-asset-manager' => 'laminas-api-tools/api-tools-asset-manager', + 'zfcampus/zf-configuration' => 'laminas-api-tools/api-tools-configuration', + 'zfcampus/zf-content-negotiation' => 'laminas-api-tools/api-tools-content-negotiation', + 'zfcampus/zf-content-validation' => 'laminas-api-tools/api-tools-content-validation', + 'zfcampus/zf-development-mode' => 'laminas/laminas-development-mode', + 'zfcampus/zf-doctrine-querybuilder' => 'laminas-api-tools/api-tools-doctrine-querybuilder', + 'zfcampus/zf-hal' => 'laminas-api-tools/api-tools-hal', + 'zfcampus/zf-http-cache' => 'laminas-api-tools/api-tools-http-cache', + 'zfcampus/zf-mvc-auth' => 'laminas-api-tools/api-tools-mvc-auth', + 'zfcampus/zf-oauth2' => 'laminas-api-tools/api-tools-oauth2', + 'zfcampus/zf-rest' => 'laminas-api-tools/api-tools-rest', + 'zfcampus/zf-rpc' => 'laminas-api-tools/api-tools-rpc', + 'zfcampus/zf-versioning' => 'laminas-api-tools/api-tools-versioning', + + // CONFIG KEYS, SCRIPT NAMES, ETC + // ZF components + '::fromZend' => '::fromLaminas', // psr7bridge + '::toZend' => '::toLaminas', // psr7bridge + 'use_zend_loader' => 'use_laminas_loader', // zend-modulemanager + 'zend-config' => 'laminas-config', + 'zend-developer-tools/' => 'laminas-developer-tools/', + 'zend-tag-cloud' => 'laminas-tag-cloud', + 'zenddevelopertools' => 'laminas-developer-tools', + 'zendbarcode' => 'laminasbarcode', + 'ZendBarcode' => 'LaminasBarcode', + 'zendcache' => 'laminascache', + 'ZendCache' => 'LaminasCache', + 'zendconfig' => 'laminasconfig', + 'ZendConfig' => 'LaminasConfig', + 'zendfeed' => 'laminasfeed', + 'ZendFeed' => 'LaminasFeed', + 'zendfilter' => 'laminasfilter', + 'ZendFilter' => 'LaminasFilter', + 'zendform' => 'laminasform', + 'ZendForm' => 'LaminasForm', + 'zendi18n' => 'laminasi18n', + 'ZendI18n' => 'LaminasI18n', + 'zendinputfilter' => 'laminasinputfilter', + 'ZendInputFilter' => 'LaminasInputFilter', + 'zendlog' => 'laminaslog', + 'ZendLog' => 'LaminasLog', + 'zendmail' => 'laminasmail', + 'ZendMail' => 'LaminasMail', + 'zendmvc' => 'laminasmvc', + 'ZendMvc' => 'LaminasMvc', + 'zendpaginator' => 'laminaspaginator', + 'ZendPaginator' => 'LaminasPaginator', + 'zendserializer' => 'laminasserializer', + 'ZendSerializer' => 'LaminasSerializer', + 'zendtag' => 'laminastag', + 'ZendTag' => 'LaminasTag', + 'zendtext' => 'laminastext', + 'ZendText' => 'LaminasText', + 'zendvalidator' => 'laminasvalidator', + 'ZendValidator' => 'LaminasValidator', + 'zendview' => 'laminasview', + 'ZendView' => 'LaminasView', + 'zend-framework.flf' => 'laminas-project.flf', + + // Expressive-related + "'zend-expressive'" => "'mezzio'", + '"zend-expressive"' => '"mezzio"', + 'zend-expressive.' => 'mezzio.', + 'zend-expressive-authorization' => 'mezzio-authorization', + 'zend-expressive-hal' => 'mezzio-hal', + 'zend-expressive-session' => 'mezzio-session', + 'zend-expressive-swoole' => 'mezzio-swoole', + 'zend-expressive-tooling' => 'mezzio-tooling', + + // Apigility-related + "'zf-apigility'" => "'laminas-api-tools'", + '"zf-apigility"' => '"laminas-api-tools"', + 'zf-apigility/' => 'api-tools/', + 'zf-apigility-admin' => 'api-tools-admin', + 'zf-content-negotiation' => 'api-tools-content-negotiation', + 'zf-hal' => 'api-tools-hal', + 'zf-rest' => 'api-tools-rest', + 'zf-rpc' => 'api-tools-rpc', + 'zf-content-validation' => 'api-tools-content-validation', + 'zf-apigility-ui' => 'api-tools-ui', + 'zf-apigility-documentation-blueprint' => 'api-tools-documentation-blueprint', + 'zf-apigility-documentation-swagger' => 'api-tools-documentation-swagger', + 'zf-apigility-welcome' => 'api-tools-welcome', + 'zf-api-problem' => 'api-tools-api-problem', + 'zf-configuration' => 'api-tools-configuration', + 'zf-http-cache' => 'api-tools-http-cache', + 'zf-mvc-auth' => 'api-tools-mvc-auth', + 'zf-oauth2' => 'api-tools-oauth2', + 'zf-versioning' => 'api-tools-versioning', + 'ZfApigilityDoctrineQueryProviderManager' => 'LaminasApiToolsDoctrineQueryProviderManager', + 'ZfApigilityDoctrineQueryCreateFilterManager' => 'LaminasApiToolsDoctrineQueryCreateFilterManager', + 'zf-apigility-doctrine' => 'api-tools-doctrine', + 'zf-development-mode' => 'laminas-development-mode', +]; diff --git a/kirby/vendor/laminas/laminas-zendframework-bridge/src/Autoloader.php b/kirby/vendor/laminas/laminas-zendframework-bridge/src/Autoloader.php new file mode 100755 index 0000000..13a28db --- /dev/null +++ b/kirby/vendor/laminas/laminas-zendframework-bridge/src/Autoloader.php @@ -0,0 +1,156 @@ +loadClass($class)) { + $legacy = $namespaces[$check] + . strtr(substr($class, strlen($check)), [ + 'ApiTools' => 'Apigility', + 'Mezzio' => 'Expressive', + 'Laminas' => 'Zend', + ]); + class_alias($class, $legacy); + } + }; + } + + /** + * @return callable + */ + private static function createAppendAutoloader(array $namespaces, ArrayObject $loaded) + { + /** + * @param string $class Class name to autoload + * @return void + */ + return static function ($class) use ($namespaces, $loaded) { + $segments = explode('\\', $class); + + if ($segments[0] === 'ZendService' && isset($segments[1])) { + $segments[0] .= '\\' . $segments[1]; + unset($segments[1]); + $segments = array_values($segments); + } + + $i = 0; + $check = ''; + + // We are checking segments of the namespace to match quicker + while (isset($segments[$i + 1], $namespaces[$check . $segments[$i] . '\\'])) { + $check .= $segments[$i] . '\\'; + ++$i; + } + + if ($check === '') { + return; + } + + $alias = $namespaces[$check] + . strtr(substr($class, strlen($check)), [ + 'Apigility' => 'ApiTools', + 'Expressive' => 'Mezzio', + 'Zend' => 'Laminas', + 'AbstractZendServer' => 'AbstractZendServer', + 'ZendServerDisk' => 'ZendServerDisk', + 'ZendServerShm' => 'ZendServerShm', + 'ZendMonitor' => 'ZendMonitor', + ]); + + $loaded[$alias] = true; + if (class_exists($alias) || interface_exists($alias) || trait_exists($alias)) { + class_alias($alias, $class); + } + }; + } +} diff --git a/kirby/vendor/laminas/laminas-zendframework-bridge/src/ConfigPostProcessor.php b/kirby/vendor/laminas/laminas-zendframework-bridge/src/ConfigPostProcessor.php new file mode 100755 index 0000000..6fb38ce --- /dev/null +++ b/kirby/vendor/laminas/laminas-zendframework-bridge/src/ConfigPostProcessor.php @@ -0,0 +1,263 @@ + string values */ + private $exactReplacements = [ + 'zend-expressive' => 'mezzio', + 'zf-apigility' => 'api-tools', + ]; + + /** @var Replacements */ + private $replacements; + + /** @var callable[] */ + private $rulesets; + + public function __construct() + { + $this->replacements = new Replacements(); + + /* Define the rulesets for replacements. + * + * Each ruleset has the following signature: + * + * @param mixed $value + * @param string[] $keys Full nested key hierarchy leading to the value + * @return null|callable + * + * If no match is made, a null is returned, allowing it to fallback to + * the next ruleset in the list. If a match is made, a callback is returned, + * and that will be used to perform the replacement on the value. + * + * The callback should have the following signature: + * + * @param mixed $value + * @param string[] $keys + * @return mixed The transformed value + */ + $this->rulesets = [ + // Exact values + function ($value) { + return is_string($value) && isset($this->exactReplacements[$value]) + ? [$this, 'replaceExactValue'] + : null; + }, + + // Router (MVC applications) + // We do not want to rewrite these. + function ($value, array $keys) { + $key = array_pop($keys); + // Only worried about a top-level "router" key. + return $key === 'router' && count($keys) === 0 && is_array($value) + ? [$this, 'noopReplacement'] + : null; + }, + + // Aliases and invokables + function ($value, array $keys) { + static $keysOfInterest; + + $keysOfInterest = $keysOfInterest ?: ['aliases', 'invokables']; + $key = array_pop($keys); + + return in_array($key, $keysOfInterest, true) && is_array($value) + ? [$this, 'replaceDependencyAliases'] + : null; + }, + + // Array values + function ($value, array $keys) { + return 0 !== count($keys) && is_array($value) + ? [$this, '__invoke'] + : null; + }, + ]; + } + + /** + * @param string[] $keys Hierarchy of keys, for determining location in + * nested configuration. + * @return array + */ + public function __invoke(array $config, array $keys = []) + { + $rewritten = []; + + foreach ($config as $key => $value) { + // Determine new key from replacements + $newKey = is_string($key) ? $this->replace($key, $keys) : $key; + + // Keep original values with original key, if the key has changed, but only at the top-level. + if (empty($keys) && $newKey !== $key) { + $rewritten[$key] = $value; + } + + // Perform value replacements, if any + $newValue = $this->replace($value, $keys, $newKey); + + // Key does not already exist and/or is not an array value + if (! array_key_exists($newKey, $rewritten) || ! is_array($rewritten[$newKey])) { + // Do not overwrite existing values with null values + $rewritten[$newKey] = array_key_exists($newKey, $rewritten) && null === $newValue + ? $rewritten[$newKey] + : $newValue; + continue; + } + + // New value is null; nothing to do. + if (null === $newValue) { + continue; + } + + // Key already exists as an array value, but $value is not an array + if (! is_array($newValue)) { + $rewritten[$newKey][] = $newValue; + continue; + } + + // Key already exists as an array value, and $value is also an array + $rewritten[$newKey] = static::merge($rewritten[$newKey], $newValue); + } + + return $rewritten; + } + + /** + * Perform substitutions as needed on an individual value. + * + * The $key is provided to allow fine-grained selection of rewrite rules. + * + * @param mixed $value + * @param string[] $keys Key hierarchy + * @param null|int|string $key + * @return mixed + */ + private function replace($value, array $keys, $key = null) + { + // Add new key to the list of keys. + // We do not need to remove it later, as we are working on a copy of the array. + array_push($keys, $key); + + // Identify rewrite strategy and perform replacements + $rewriteRule = $this->replacementRuleMatch($value, $keys); + return $rewriteRule($value, $keys); + } + + /** + * Merge two arrays together. + * + * If an integer key exists in both arrays, the value from the second array + * will be appended to the first array. If both values are arrays, they are + * merged together, else the value of the second array overwrites the one + * of the first array. + * + * Based on zend-stdlib Zend\Stdlib\ArrayUtils::merge + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * + * @return array + */ + public static function merge(array $a, array $b) + { + foreach ($b as $key => $value) { + if (! isset($a[$key]) && ! array_key_exists($key, $a)) { + $a[$key] = $value; + continue; + } + + if (null === $value && array_key_exists($key, $a)) { + // Leave as-is if value from $b is null + continue; + } + + if (is_int($key)) { + $a[] = $value; + continue; + } + + if (is_array($value) && is_array($a[$key])) { + $a[$key] = static::merge($a[$key], $value); + continue; + } + + $a[$key] = $value; + } + + return $a; + } + + /** + * @param mixed $value + * @param null|int|string $key + * @return callable Callable to invoke with value + */ + private function replacementRuleMatch($value, $key = null) + { + foreach ($this->rulesets as $ruleset) { + $result = $ruleset($value, $key); + if (is_callable($result)) { + return $result; + } + } + return [$this, 'fallbackReplacement']; + } + + /** + * Replace a value using the translation table, if the value is a string. + * + * @param mixed $value + * @return mixed + */ + private function fallbackReplacement($value) + { + return is_string($value) + ? $this->replacements->replace($value) + : $value; + } + + /** + * Replace a value matched exactly. + * + * @param mixed $value + * @return mixed + */ + private function replaceExactValue($value) + { + return $this->exactReplacements[$value]; + } + + /** + * Rewrite dependency aliases array + * + * In this case, we want to keep the alias as-is, but rewrite the target. + * + * This same logic can be used for invokables, which are essentially just + * an alias map. + * + * @return array + */ + private function replaceDependencyAliases(array $aliases) + { + foreach ($aliases as $alias => $target) { + $aliases[$alias] = $this->replacements->replace($target); + } + return $aliases; + } + + /** + * @param mixed $value + * @return mixed Returns $value verbatim. + */ + private function noopReplacement($value) + { + return $value; + } +} diff --git a/kirby/vendor/laminas/laminas-zendframework-bridge/src/Module.php b/kirby/vendor/laminas/laminas-zendframework-bridge/src/Module.php new file mode 100755 index 0000000..d10cb43 --- /dev/null +++ b/kirby/vendor/laminas/laminas-zendframework-bridge/src/Module.php @@ -0,0 +1,54 @@ +getEventManager() + ->attach('mergeConfig', [$this, 'onMergeConfig']); + } + + /** + * Perform substitutions in the merged configuration. + * + * Rewrites keys and values matching known ZF classes, namespaces, and + * configuration keys to their Laminas equivalents. + * + * Type-hinting deliberately omitted to allow unit testing + * without dependencies on packages that do not exist yet. + * + * @param ModuleEvent $event + */ + public function onMergeConfig($event) + { + /** @var ConfigMergerInterface */ + $configMerger = $event->getConfigListener(); + $processor = new ConfigPostProcessor(); + $configMerger->setMergedConfig( + $processor( + $configMerger->getMergedConfig($returnAsObject = false) + ) + ); + } +} diff --git a/kirby/vendor/laminas/laminas-zendframework-bridge/src/Replacements.php b/kirby/vendor/laminas/laminas-zendframework-bridge/src/Replacements.php new file mode 100755 index 0000000..ba664b1 --- /dev/null +++ b/kirby/vendor/laminas/laminas-zendframework-bridge/src/Replacements.php @@ -0,0 +1,32 @@ +replacements = array_merge( + require __DIR__ . '/../config/replacements.php', + $additionalReplacements + ); + } + + /** + * @param string $value + * @return string + */ + public function replace($value) + { + return strtr($value, $this->replacements); + } +} diff --git a/kirby/vendor/laminas/laminas-zendframework-bridge/src/RewriteRules.php b/kirby/vendor/laminas/laminas-zendframework-bridge/src/RewriteRules.php new file mode 100755 index 0000000..8dc999f --- /dev/null +++ b/kirby/vendor/laminas/laminas-zendframework-bridge/src/RewriteRules.php @@ -0,0 +1,79 @@ + 'Mezzio\\ProblemDetails\\', + 'Zend\\Expressive\\' => 'Mezzio\\', + + // Laminas + 'Zend\\' => 'Laminas\\', + 'ZF\\ComposerAutoloading\\' => 'Laminas\\ComposerAutoloading\\', + 'ZF\\DevelopmentMode\\' => 'Laminas\\DevelopmentMode\\', + + // Apigility + 'ZF\\Apigility\\' => 'Laminas\\ApiTools\\', + 'ZF\\' => 'Laminas\\ApiTools\\', + + // ZendXml, API wrappers, zend-http OAuth support, zend-diagnostics, ZendDeveloperTools + 'ZendXml\\' => 'Laminas\\Xml\\', + 'ZendOAuth\\' => 'Laminas\\OAuth\\', + 'ZendDiagnostics\\' => 'Laminas\\Diagnostics\\', + 'ZendService\\ReCaptcha\\' => 'Laminas\\ReCaptcha\\', + 'ZendService\\Twitter\\' => 'Laminas\\Twitter\\', + 'ZendDeveloperTools\\' => 'Laminas\\DeveloperTools\\', + ]; + } + + /** + * @return array + */ + public static function namespaceReverse() + { + return [ + // ZendXml, ZendOAuth, ZendDiagnostics, ZendDeveloperTools + 'Laminas\\Xml\\' => 'ZendXml\\', + 'Laminas\\OAuth\\' => 'ZendOAuth\\', + 'Laminas\\Diagnostics\\' => 'ZendDiagnostics\\', + 'Laminas\\DeveloperTools\\' => 'ZendDeveloperTools\\', + + // Zend Service + 'Laminas\\ReCaptcha\\' => 'ZendService\\ReCaptcha\\', + 'Laminas\\Twitter\\' => 'ZendService\\Twitter\\', + + // Zend + 'Laminas\\' => 'Zend\\', + + // Expressive + 'Mezzio\\ProblemDetails\\' => 'Zend\\ProblemDetails\\', + 'Mezzio\\' => 'Zend\\Expressive\\', + + // Laminas to ZfCampus + 'Laminas\\ComposerAutoloading\\' => 'ZF\\ComposerAutoloading\\', + 'Laminas\\DevelopmentMode\\' => 'ZF\\DevelopmentMode\\', + + // Apigility + 'Laminas\\ApiTools\\Admin\\' => 'ZF\\Apigility\\Admin\\', + 'Laminas\\ApiTools\\Doctrine\\' => 'ZF\\Apigility\\Doctrine\\', + 'Laminas\\ApiTools\\Documentation\\' => 'ZF\\Apigility\\Documentation\\', + 'Laminas\\ApiTools\\Example\\' => 'ZF\\Apigility\\Example\\', + 'Laminas\\ApiTools\\Provider\\' => 'ZF\\Apigility\\Provider\\', + 'Laminas\\ApiTools\\Welcome\\' => 'ZF\\Apiglity\\Welcome\\', + 'Laminas\\ApiTools\\' => 'ZF\\', + ]; + } +} diff --git a/kirby/vendor/laminas/laminas-zendframework-bridge/src/autoload.php b/kirby/vendor/laminas/laminas-zendframework-bridge/src/autoload.php new file mode 100755 index 0000000..09ec2ea --- /dev/null +++ b/kirby/vendor/laminas/laminas-zendframework-bridge/src/autoload.php @@ -0,0 +1,9 @@ + $color >> 16 & 0xFF, + 'g' => $color >> 8 & 0xFF, + 'b' => $color & 0xFF, + ]; + } + + /** + * @param array $components + * + * @return int + */ + public static function fromRgbToInt(array $components) + { + return ($components['r'] * 65536) + ($components['g'] * 256) + ($components['b']); + } +} diff --git a/kirby/vendor/league/color-extractor/src/League/ColorExtractor/ColorExtractor.php b/kirby/vendor/league/color-extractor/src/League/ColorExtractor/ColorExtractor.php new file mode 100755 index 0000000..09e43c1 --- /dev/null +++ b/kirby/vendor/league/color-extractor/src/League/ColorExtractor/ColorExtractor.php @@ -0,0 +1,275 @@ +palette = $palette; + } + + /** + * @param int $colorCount + * + * @return array + */ + public function extract($colorCount = 1) + { + if (!$this->isInitialized()) { + $this->initialize(); + } + + return self::mergeColors($this->sortedColors, $colorCount, 100 / $colorCount); + } + + /** + * @return bool + */ + protected function isInitialized() + { + return $this->sortedColors !== null; + } + + protected function initialize() + { + $queue = new \SplPriorityQueue(); + $this->sortedColors = new \SplFixedArray(count($this->palette)); + + $i = 0; + foreach ($this->palette as $color => $count) { + $labColor = self::intColorToLab($color); + $queue->insert( + $color, + (sqrt($labColor['a'] * $labColor['a'] + $labColor['b'] * $labColor['b']) ?: 1) * + (1 - $labColor['L'] / 200) * + sqrt($count) + ); + ++$i; + } + + $i = 0; + while ($queue->valid()) { + $this->sortedColors[$i] = $queue->current(); + $queue->next(); + ++$i; + } + } + + /** + * @param \SplFixedArray $colors + * @param int $limit + * @param int $maxDelta + * + * @return array + */ + protected static function mergeColors(\SplFixedArray $colors, $limit, $maxDelta) + { + $limit = min(count($colors), $limit); + if ($limit === 1) { + return [$colors[0]]; + } + $labCache = new \SplFixedArray($limit - 1); + $mergedColors = []; + + foreach ($colors as $color) { + $hasColorBeenMerged = false; + + $colorLab = self::intColorToLab($color); + + foreach ($mergedColors as $i => $mergedColor) { + if (self::ciede2000DeltaE($colorLab, $labCache[$i]) < $maxDelta) { + $hasColorBeenMerged = true; + break; + } + } + + if ($hasColorBeenMerged) { + continue; + } + + $mergedColorCount = count($mergedColors); + $mergedColors[] = $color; + + if ($mergedColorCount + 1 == $limit) { + break; + } + + $labCache[$mergedColorCount] = $colorLab; + } + + return $mergedColors; + } + + /** + * @param array $firstLabColor + * @param array $secondLabColor + * + * @return float + */ + protected static function ciede2000DeltaE($firstLabColor, $secondLabColor) + { + $C1 = sqrt(pow($firstLabColor['a'], 2) + pow($firstLabColor['b'], 2)); + $C2 = sqrt(pow($secondLabColor['a'], 2) + pow($secondLabColor['b'], 2)); + $Cb = ($C1 + $C2) / 2; + + $G = .5 * (1 - sqrt(pow($Cb, 7) / (pow($Cb, 7) + pow(25, 7)))); + + $a1p = (1 + $G) * $firstLabColor['a']; + $a2p = (1 + $G) * $secondLabColor['a']; + + $C1p = sqrt(pow($a1p, 2) + pow($firstLabColor['b'], 2)); + $C2p = sqrt(pow($a2p, 2) + pow($secondLabColor['b'], 2)); + + $h1p = $a1p == 0 && $firstLabColor['b'] == 0 ? 0 : atan2($firstLabColor['b'], $a1p); + $h2p = $a2p == 0 && $secondLabColor['b'] == 0 ? 0 : atan2($secondLabColor['b'], $a2p); + + $LpDelta = $secondLabColor['L'] - $firstLabColor['L']; + $CpDelta = $C2p - $C1p; + + if ($C1p * $C2p == 0) { + $hpDelta = 0; + } elseif (abs($h2p - $h1p) <= 180) { + $hpDelta = $h2p - $h1p; + } elseif ($h2p - $h1p > 180) { + $hpDelta = $h2p - $h1p - 360; + } else { + $hpDelta = $h2p - $h1p + 360; + } + + $HpDelta = 2 * sqrt($C1p * $C2p) * sin($hpDelta / 2); + + $Lbp = ($firstLabColor['L'] + $secondLabColor['L']) / 2; + $Cbp = ($C1p + $C2p) / 2; + + if ($C1p * $C2p == 0) { + $hbp = $h1p + $h2p; + } elseif (abs($h1p - $h2p) <= 180) { + $hbp = ($h1p + $h2p) / 2; + } elseif ($h1p + $h2p < 360) { + $hbp = ($h1p + $h2p + 360) / 2; + } else { + $hbp = ($h1p + $h2p - 360) / 2; + } + + $T = 1 - .17 * cos($hbp - 30) + .24 * cos(2 * $hbp) + .32 * cos(3 * $hbp + 6) - .2 * cos(4 * $hbp - 63); + + $sigmaDelta = 30 * exp(-pow(($hbp - 275) / 25, 2)); + + $Rc = 2 * sqrt(pow($Cbp, 7) / (pow($Cbp, 7) + pow(25, 7))); + + $Sl = 1 + ((.015 * pow($Lbp - 50, 2)) / sqrt(20 + pow($Lbp - 50, 2))); + $Sc = 1 + .045 * $Cbp; + $Sh = 1 + .015 * $Cbp * $T; + + $Rt = -sin(2 * $sigmaDelta) * $Rc; + + return sqrt( + pow($LpDelta / $Sl, 2) + + pow($CpDelta / $Sc, 2) + + pow($HpDelta / $Sh, 2) + + $Rt * ($CpDelta / $Sc) * ($HpDelta / $Sh) + ); + } + + /** + * @param int $color + * + * @return array + */ + protected static function intColorToLab($color) + { + return self::xyzToLab( + self::srgbToXyz( + self::rgbToSrgb( + [ + 'R' => ($color >> 16) & 0xFF, + 'G' => ($color >> 8) & 0xFF, + 'B' => $color & 0xFF, + ] + ) + ) + ); + } + + /** + * @param int $value + * + * @return float + */ + protected static function rgbToSrgbStep($value) + { + $value /= 255; + + return $value <= .03928 ? + $value / 12.92 : + pow(($value + .055) / 1.055, 2.4); + } + + /** + * @param array $rgb + * + * @return array + */ + protected static function rgbToSrgb($rgb) + { + return [ + 'R' => self::rgbToSrgbStep($rgb['R']), + 'G' => self::rgbToSrgbStep($rgb['G']), + 'B' => self::rgbToSrgbStep($rgb['B']), + ]; + } + + /** + * @param array $rgb + * + * @return array + */ + protected static function srgbToXyz($rgb) + { + return [ + 'X' => (.4124564 * $rgb['R']) + (.3575761 * $rgb['G']) + (.1804375 * $rgb['B']), + 'Y' => (.2126729 * $rgb['R']) + (.7151522 * $rgb['G']) + (.0721750 * $rgb['B']), + 'Z' => (.0193339 * $rgb['R']) + (.1191920 * $rgb['G']) + (.9503041 * $rgb['B']), + ]; + } + + /** + * @param float $value + * + * @return float + */ + protected static function xyzToLabStep($value) + { + return $value > 216 / 24389 ? pow($value, 1 / 3) : 841 * $value / 108 + 4 / 29; + } + + /** + * @param array $xyz + * + * @return array + */ + protected static function xyzToLab($xyz) + { + //http://en.wikipedia.org/wiki/Illuminant_D65#Definition + $Xn = .95047; + $Yn = 1; + $Zn = 1.08883; + + // http://en.wikipedia.org/wiki/Lab_color_space#CIELAB-CIEXYZ_conversions + return [ + 'L' => 116 * self::xyzToLabStep($xyz['Y'] / $Yn) - 16, + 'a' => 500 * (self::xyzToLabStep($xyz['X'] / $Xn) - self::xyzToLabStep($xyz['Y'] / $Yn)), + 'b' => 200 * (self::xyzToLabStep($xyz['Y'] / $Yn) - self::xyzToLabStep($xyz['Z'] / $Zn)), + ]; + } +} diff --git a/kirby/vendor/league/color-extractor/src/League/ColorExtractor/Palette.php b/kirby/vendor/league/color-extractor/src/League/ColorExtractor/Palette.php new file mode 100755 index 0000000..d8fb4f9 --- /dev/null +++ b/kirby/vendor/league/color-extractor/src/League/ColorExtractor/Palette.php @@ -0,0 +1,126 @@ +colors); + } + + /** + * @return \ArrayIterator + */ + public function getIterator() + { + return new \ArrayIterator($this->colors); + } + + /** + * @param int $color + * + * @return int + */ + public function getColorCount($color) + { + return $this->colors[$color]; + } + + /** + * @param int $limit = null + * + * @return array + */ + public function getMostUsedColors($limit = null) + { + return array_slice($this->colors, 0, $limit, true); + } + + /** + * @param string $filename + * @param int|null $backgroundColor + * + * @return Palette + */ + public static function fromFilename($filename, $backgroundColor = null) + { + $image = imagecreatefromstring(file_get_contents($filename)); + $palette = self::fromGD($image, $backgroundColor); + imagedestroy($image); + + return $palette; + } + + /** + * @param resource $image + * @param int|null $backgroundColor + * + * @return Palette + * + * @throws \InvalidArgumentException + */ + public static function fromGD($image, $backgroundColor = null) + { + if (!is_resource($image) || get_resource_type($image) != 'gd') { + throw new \InvalidArgumentException('Image must be a gd resource'); + } + if ($backgroundColor !== null && (!is_numeric($backgroundColor) || $backgroundColor < 0 || $backgroundColor > 16777215)) { + throw new \InvalidArgumentException(sprintf('"%s" does not represent a valid color', $backgroundColor)); + } + + $palette = new self(); + + $areColorsIndexed = !imageistruecolor($image); + $imageWidth = imagesx($image); + $imageHeight = imagesy($image); + $palette->colors = []; + + $backgroundColorRed = ($backgroundColor >> 16) & 0xFF; + $backgroundColorGreen = ($backgroundColor >> 8) & 0xFF; + $backgroundColorBlue = $backgroundColor & 0xFF; + + for ($x = 0; $x < $imageWidth; ++$x) { + for ($y = 0; $y < $imageHeight; ++$y) { + $color = imagecolorat($image, $x, $y); + if ($areColorsIndexed) { + $colorComponents = imagecolorsforindex($image, $color); + $color = ($colorComponents['alpha'] * 16777216) + + ($colorComponents['red'] * 65536) + + ($colorComponents['green'] * 256) + + ($colorComponents['blue']); + } + + if ($alpha = $color >> 24) { + if ($backgroundColor === null) { + continue; + } + + $alpha /= 127; + $color = (int) (($color >> 16 & 0xFF) * (1 - $alpha) + $backgroundColorRed * $alpha) * 65536 + + (int) (($color >> 8 & 0xFF) * (1 - $alpha) + $backgroundColorGreen * $alpha) * 256 + + (int) (($color & 0xFF) * (1 - $alpha) + $backgroundColorBlue * $alpha); + } + + isset($palette->colors[$color]) ? + $palette->colors[$color] += 1 : + $palette->colors[$color] = 1; + } + } + + arsort($palette->colors); + + return $palette; + } + + protected function __construct() + { + $this->colors = []; + } +} diff --git a/kirby/vendor/michelf/php-smartypants/Michelf/SmartyPants.inc.php b/kirby/vendor/michelf/php-smartypants/Michelf/SmartyPants.inc.php new file mode 100755 index 0000000..b4ee661 --- /dev/null +++ b/kirby/vendor/michelf/php-smartypants/Michelf/SmartyPants.inc.php @@ -0,0 +1,9 @@ + +# +# Original SmartyPants +# Copyright (c) 2003-2004 John Gruber +# +# +namespace Michelf; + + +# +# SmartyPants Parser Class +# + +class SmartyPants { + + ### Version ### + + const SMARTYPANTSLIB_VERSION = "1.8.1"; + + + ### Presets + + # SmartyPants does nothing at all + const ATTR_DO_NOTHING = 0; + # "--" for em-dashes; no en-dash support + const ATTR_EM_DASH = 1; + # "---" for em-dashes; "--" for en-dashes + const ATTR_LONG_EM_DASH_SHORT_EN = 2; + # "--" for em-dashes; "---" for en-dashes + const ATTR_SHORT_EM_DASH_LONG_EN = 3; + # "--" for em-dashes; "---" for en-dashes + const ATTR_STUPEFY = -1; + + # The default preset: ATTR_EM_DASH + const ATTR_DEFAULT = SmartyPants::ATTR_EM_DASH; + + + ### Standard Function Interface ### + + public static function defaultTransform($text, $attr = SmartyPants::ATTR_DEFAULT) { + # + # Initialize the parser and return the result of its transform method. + # This will work fine for derived classes too. + # + # Take parser class on which this function was called. + $parser_class = \get_called_class(); + + # try to take parser from the static parser list + static $parser_list; + $parser =& $parser_list[$parser_class][$attr]; + + # create the parser if not already set + if (!$parser) + $parser = new $parser_class($attr); + + # Transform text using parser. + return $parser->transform($text); + } + + + ### Configuration Variables ### + + # Partial regex for matching tags to skip + public $tags_to_skip = 'pre|code|kbd|script|style|math'; + + # Options to specify which transformations to make: + public $do_nothing = 0; # disable all transforms + public $do_quotes = 0; + public $do_backticks = 0; # 1 => double only, 2 => double & single + public $do_dashes = 0; # 1, 2, or 3 for the three modes described above + public $do_ellipses = 0; + public $do_stupefy = 0; + public $convert_quot = 0; # should we translate " entities into normal quotes? + + # Smart quote characters: + # Opening and closing smart double-quotes. + public $smart_doublequote_open = '“'; + public $smart_doublequote_close = '”'; + public $smart_singlequote_open = '‘'; + public $smart_singlequote_close = '’'; # Also apostrophe. + + # ``Backtick quotes'' + public $backtick_doublequote_open = '“'; // replacement for `` + public $backtick_doublequote_close = '”'; // replacement for '' + public $backtick_singlequote_open = '‘'; // replacement for ` + public $backtick_singlequote_close = '’'; // replacement for ' (also apostrophe) + + # Other punctuation + public $em_dash = '—'; + public $en_dash = '–'; + public $ellipsis = '…'; + + ### Parser Implementation ### + + public function __construct($attr = SmartyPants::ATTR_DEFAULT) { + # + # Initialize a parser with certain attributes. + # + # Parser attributes: + # 0 : do nothing + # 1 : set all + # 2 : set all, using old school en- and em- dash shortcuts + # 3 : set all, using inverted old school en and em- dash shortcuts + # + # q : quotes + # b : backtick quotes (``double'' only) + # B : backtick quotes (``double'' and `single') + # d : dashes + # D : old school dashes + # i : inverted old school dashes + # e : ellipses + # w : convert " entities to " for Dreamweaver users + # + if ($attr == "0") { + $this->do_nothing = 1; + } + else if ($attr == "1") { + # Do everything, turn all options on. + $this->do_quotes = 1; + $this->do_backticks = 1; + $this->do_dashes = 1; + $this->do_ellipses = 1; + } + else if ($attr == "2") { + # Do everything, turn all options on, use old school dash shorthand. + $this->do_quotes = 1; + $this->do_backticks = 1; + $this->do_dashes = 2; + $this->do_ellipses = 1; + } + else if ($attr == "3") { + # Do everything, turn all options on, use inverted old school dash shorthand. + $this->do_quotes = 1; + $this->do_backticks = 1; + $this->do_dashes = 3; + $this->do_ellipses = 1; + } + else if ($attr == "-1") { + # Special "stupefy" mode. + $this->do_stupefy = 1; + } + else { + $chars = preg_split('//', $attr); + foreach ($chars as $c){ + if ($c == "q") { $this->do_quotes = 1; } + else if ($c == "b") { $this->do_backticks = 1; } + else if ($c == "B") { $this->do_backticks = 2; } + else if ($c == "d") { $this->do_dashes = 1; } + else if ($c == "D") { $this->do_dashes = 2; } + else if ($c == "i") { $this->do_dashes = 3; } + else if ($c == "e") { $this->do_ellipses = 1; } + else if ($c == "w") { $this->convert_quot = 1; } + else { + # Unknown attribute option, ignore. + } + } + } + } + + public function transform($text) { + + if ($this->do_nothing) { + return $text; + } + + $tokens = $this->tokenizeHTML($text); + $result = ''; + $in_pre = 0; # Keep track of when we're inside
 or  tags.
+
+		$prev_token_last_char = ""; # This is a cheat, used to get some context
+									# for one-character tokens that consist of 
+									# just a quote char. What we do is remember
+									# the last character of the previous text
+									# token, to use as context to curl single-
+									# character quote tokens correctly.
+
+		foreach ($tokens as $cur_token) {
+			if ($cur_token[0] == "tag") {
+				# Don't mess with quotes inside tags.
+				$result .= $cur_token[1];
+				if (preg_match('@<(/?)(?:'.$this->tags_to_skip.')[\s>]@', $cur_token[1], $matches)) {
+					$in_pre = isset($matches[1]) && $matches[1] == '/' ? 0 : 1;
+				}
+			} else {
+				$t = $cur_token[1];
+				$last_char = substr($t, -1); # Remember last char of this token before processing.
+				if (! $in_pre) {
+					$t = $this->educate($t, $prev_token_last_char);
+				}
+				$prev_token_last_char = $last_char;
+				$result .= $t;
+			}
+		}
+
+		return $result;
+	}
+
+
+	function decodeEntitiesInConfiguration() {
+	#
+	#   Utility function that converts entities in configuration variables to
+	#   UTF-8 characters.
+	#
+		$output_config_vars = array(
+			'smart_doublequote_open',
+			'smart_doublequote_close',
+			'smart_singlequote_open',
+			'smart_singlequote_close',
+			'backtick_doublequote_open',
+			'backtick_doublequote_close',
+			'backtick_singlequote_open',
+			'backtick_singlequote_close',
+			'em_dash',
+			'en_dash',
+			'ellipsis',
+		);
+		foreach ($output_config_vars as $var) {
+			$this->$var = html_entity_decode($this->$var);
+		}
+	}
+
+
+	protected function educate($t, $prev_token_last_char) {
+		$t = $this->processEscapes($t);
+
+		if ($this->convert_quot) {
+			$t = preg_replace('/"/', '"', $t);
+		}
+
+		if ($this->do_dashes) {
+			if ($this->do_dashes == 1) $t = $this->educateDashes($t);
+			if ($this->do_dashes == 2) $t = $this->educateDashesOldSchool($t);
+			if ($this->do_dashes == 3) $t = $this->educateDashesOldSchoolInverted($t);
+		}
+
+		if ($this->do_ellipses) $t = $this->educateEllipses($t);
+
+		# Note: backticks need to be processed before quotes.
+		if ($this->do_backticks) {
+			$t = $this->educateBackticks($t);
+			if ($this->do_backticks == 2) $t = $this->educateSingleBackticks($t);
+		}
+
+		if ($this->do_quotes) {
+			if ($t == "'") {
+				# Special case: single-character ' token
+				if (preg_match('/\S/', $prev_token_last_char)) {
+					$t = $this->smart_singlequote_close;
+				}
+				else {
+					$t = $this->smart_singlequote_open;
+				}
+			}
+			else if ($t == '"') {
+				# Special case: single-character " token
+				if (preg_match('/\S/', $prev_token_last_char)) {
+					$t = $this->smart_doublequote_close;
+				}
+				else {
+					$t = $this->smart_doublequote_open;
+				}
+			}
+			else {
+				# Normal case:
+				$t = $this->educateQuotes($t);
+			}
+		}
+
+		if ($this->do_stupefy) $t = $this->stupefyEntities($t);
+		
+		return $t;
+	}
+
+
+	protected function educateQuotes($_) {
+	#
+	#   Parameter:  String.
+	#
+	#   Returns:    The string, with "educated" curly quote HTML entities.
+	#
+	#   Example input:  "Isn't this fun?"
+	#   Example output: “Isn’t this fun?”
+	#
+		$dq_open  = $this->smart_doublequote_open;
+		$dq_close = $this->smart_doublequote_close;
+		$sq_open  = $this->smart_singlequote_open;
+		$sq_close = $this->smart_singlequote_close;
+	
+		# Make our own "punctuation" character class, because the POSIX-style
+		# [:PUNCT:] is only available in Perl 5.6 or later:
+		$punct_class = "[!\"#\\$\\%'()*+,-.\\/:;<=>?\\@\\[\\\\\]\\^_`{|}~]";
+
+		# Special case if the very first character is a quote
+		# followed by punctuation at a non-word-break. Close the quotes by brute force:
+		$_ = preg_replace(
+			array("/^'(?=$punct_class\\B)/", "/^\"(?=$punct_class\\B)/"),
+			array($sq_close,                 $dq_close), $_);
+
+		# Special case for double sets of quotes, e.g.:
+		#   

He said, "'Quoted' words in a larger quote."

+ $_ = preg_replace( + array("/\"'(?=\w)/", "/'\"(?=\w)/"), + array($dq_open.$sq_open, $sq_open.$dq_open), $_); + + # Special case for decade abbreviations (the '80s): + $_ = preg_replace("/'(?=\\d{2}s)/", $sq_close, $_); + + $close_class = '[^\ \t\r\n\[\{\(\-]'; + $dec_dashes = '&\#8211;|&\#8212;'; + + # Get most opening single quotes: + $_ = preg_replace("{ + ( + \\s | # a whitespace char, or +   | # a non-breaking space entity, or + -- | # dashes, or + &[mn]dash; | # named dash entities + $dec_dashes | # or decimal entities + &\\#x201[34]; # or hex + ) + ' # the quote + (?=\\w) # followed by a word character + }x", '\1'.$sq_open, $_); + # Single closing quotes: + $_ = preg_replace("{ + ($close_class)? + ' + (?(1)| # If $1 captured, then do nothing; + (?=\\s | s\\b) # otherwise, positive lookahead for a whitespace + ) # char or an 's' at a word ending position. This + # is a special case to handle something like: + # \"Custer's Last Stand.\" + }xi", '\1'.$sq_close, $_); + + # Any remaining single quotes should be opening ones: + $_ = str_replace("'", $sq_open, $_); + + + # Get most opening double quotes: + $_ = preg_replace("{ + ( + \\s | # a whitespace char, or +   | # a non-breaking space entity, or + -- | # dashes, or + &[mn]dash; | # named dash entities + $dec_dashes | # or decimal entities + &\\#x201[34]; # or hex + ) + \" # the quote + (?=\\w) # followed by a word character + }x", '\1'.$dq_open, $_); + + # Double closing quotes: + $_ = preg_replace("{ + ($close_class)? + \" + (?(1)|(?=\\s)) # If $1 captured, then do nothing; + # if not, then make sure the next char is whitespace. + }x", '\1'.$dq_close, $_); + + # Any remaining quotes should be opening ones. + $_ = str_replace('"', $dq_open, $_); + + return $_; + } + + + protected function educateBackticks($_) { + # + # Parameter: String. + # Returns: The string, with ``backticks'' -style double quotes + # translated into HTML curly quote entities. + # + # Example input: ``Isn't this fun?'' + # Example output: “Isn't this fun?” + # + + $_ = str_replace(array("``", "''",), + array($this->backtick_doublequote_open, + $this->backtick_doublequote_close), $_); + return $_; + } + + + protected function educateSingleBackticks($_) { + # + # Parameter: String. + # Returns: The string, with `backticks' -style single quotes + # translated into HTML curly quote entities. + # + # Example input: `Isn't this fun?' + # Example output: ‘Isn’t this fun?’ + # + + $_ = str_replace(array("`", "'",), + array($this->backtick_singlequote_open, + $this->backtick_singlequote_close), $_); + return $_; + } + + + protected function educateDashes($_) { + # + # Parameter: String. + # + # Returns: The string, with each instance of "--" translated to + # an em-dash HTML entity. + # + + $_ = str_replace('--', $this->em_dash, $_); + return $_; + } + + + protected function educateDashesOldSchool($_) { + # + # Parameter: String. + # + # Returns: The string, with each instance of "--" translated to + # an en-dash HTML entity, and each "---" translated to + # an em-dash HTML entity. + # + + # em en + $_ = str_replace(array("---", "--",), + array($this->em_dash, $this->en_dash), $_); + return $_; + } + + + protected function educateDashesOldSchoolInverted($_) { + # + # Parameter: String. + # + # Returns: The string, with each instance of "--" translated to + # an em-dash HTML entity, and each "---" translated to + # an en-dash HTML entity. Two reasons why: First, unlike the + # en- and em-dash syntax supported by + # EducateDashesOldSchool(), it's compatible with existing + # entries written before SmartyPants 1.1, back when "--" was + # only used for em-dashes. Second, em-dashes are more + # common than en-dashes, and so it sort of makes sense that + # the shortcut should be shorter to type. (Thanks to Aaron + # Swartz for the idea.) + # + + # en em + $_ = str_replace(array("---", "--",), + array($this->en_dash, $this->em_dash), $_); + return $_; + } + + + protected function educateEllipses($_) { + # + # Parameter: String. + # Returns: The string, with each instance of "..." translated to + # an ellipsis HTML entity. Also converts the case where + # there are spaces between the dots. + # + # Example input: Huh...? + # Example output: Huh…? + # + + $_ = str_replace(array("...", ". . .",), $this->ellipsis, $_); + return $_; + } + + + protected function stupefyEntities($_) { + # + # Parameter: String. + # Returns: The string, with each SmartyPants HTML entity translated to + # its ASCII counterpart. + # + # Example input: “Hello — world.” + # Example output: "Hello -- world." + # + + # en-dash em-dash + $_ = str_replace(array('–', '—'), + array('-', '--'), $_); + + # single quote open close + $_ = str_replace(array('‘', '’'), "'", $_); + + # double quote open close + $_ = str_replace(array('“', '”'), '"', $_); + + $_ = str_replace('…', '...', $_); # ellipsis + + return $_; + } + + + protected function processEscapes($_) { + # + # Parameter: String. + # Returns: The string, with after processing the following backslash + # escape sequences. This is useful if you want to force a "dumb" + # quote or other character to appear. + # + # Escape Value + # ------ ----- + # \\ \ + # \" " + # \' ' + # \. . + # \- - + # \` ` + # + $_ = str_replace( + array('\\\\', '\"', "\'", '\.', '\-', '\`'), + array('\', '"', ''', '.', '-', '`'), $_); + + return $_; + } + + + protected function tokenizeHTML($str) { + # + # Parameter: String containing HTML markup. + # Returns: An array of the tokens comprising the input + # string. Each token is either a tag (possibly with nested, + # tags contained therein, such as , or a + # run of text between tags. Each element of the array is a + # two-element array; the first is either 'tag' or 'text'; + # the second is the actual value. + # + # + # Regular expression derived from the _tokenize() subroutine in + # Brad Choate's MTRegex plugin. + # + # + $index = 0; + $tokens = array(); + + $match = '(?s:)|'. # comment + '(?s:<\?.*?\?>)|'. # processing instruction + # regular tags + '(?:<[/!$]?[-a-zA-Z0-9:]+\b(?>[^"\'>]+|"[^"]*"|\'[^\']*\')*>)'; + + $parts = preg_split("{($match)}", $str, -1, PREG_SPLIT_DELIM_CAPTURE); + + foreach ($parts as $part) { + if (++$index % 2 && $part != '') + $tokens[] = array('text', $part); + else + $tokens[] = array('tag', $part); + } + return $tokens; + } + +} diff --git a/kirby/vendor/michelf/php-smartypants/Michelf/SmartyPantsTypographer.inc.php b/kirby/vendor/michelf/php-smartypants/Michelf/SmartyPantsTypographer.inc.php new file mode 100755 index 0000000..9b3d274 --- /dev/null +++ b/kirby/vendor/michelf/php-smartypants/Michelf/SmartyPantsTypographer.inc.php @@ -0,0 +1,10 @@ + +# +# Original SmartyPants +# Copyright (c) 2003-2004 John Gruber +# +# +namespace Michelf; + + +# +# SmartyPants Typographer Parser Class +# +class SmartyPantsTypographer extends \Michelf\SmartyPants { + + ### Configuration Variables ### + + # Options to specify which transformations to make: + public $do_comma_quotes = 0; + public $do_guillemets = 0; + public $do_geresh_gershayim = 0; + public $do_space_emdash = 0; + public $do_space_endash = 0; + public $do_space_colon = 0; + public $do_space_semicolon = 0; + public $do_space_marks = 0; + public $do_space_frenchquote = 0; + public $do_space_thousand = 0; + public $do_space_unit = 0; + + # Quote characters for replacing ASCII approximations + public $doublequote_low = "„"; // replacement for ,, + public $guillemet_leftpointing = "«"; // replacement for << + public $guillemet_rightpointing = "»"; // replacement for >> + public $geresh = "׳"; + public $gershayim = "״"; + + # Space characters for different places: + # Space around em-dashes. "He_—_or she_—_should change that." + public $space_emdash = " "; + # Space around en-dashes. "He_–_or she_–_should change that." + public $space_endash = " "; + # Space before a colon. "He said_: here it is." + public $space_colon = " "; + # Space before a semicolon. "That's what I said_; that's what he said." + public $space_semicolon = " "; + # Space before a question mark and an exclamation mark: "¡_Holà_! What_?" + public $space_marks = " "; + # Space inside french quotes. "Voici la «_chose_» qui m'a attaqué." + public $space_frenchquote = " "; + # Space as thousand separator. "On compte 10_000 maisons sur cette liste." + public $space_thousand = " "; + # Space before a unit abreviation. "This 12_kg of matter costs 10_$." + public $space_unit = " "; + + + # Expression of a space (breakable or not): + public $space = '(?: | | |�*160;|�*[aA]0;)'; + + + ### Parser Implementation ### + + public function __construct($attr = SmartyPants::ATTR_DEFAULT) { + # + # Initialize a SmartyPantsTypographer_Parser with certain attributes. + # + # Parser attributes: + # 0 : do nothing + # 1 : set all, except dash spacing + # 2 : set all, except dash spacing, using old school en- and em- dash shortcuts + # 3 : set all, except dash spacing, using inverted old school en and em- dash shortcuts + # + # Punctuation: + # q -> quotes + # b -> backtick quotes (``double'' only) + # B -> backtick quotes (``double'' and `single') + # c -> comma quotes (,,double`` only) + # g -> guillemets (<> only) + # d -> dashes + # D -> old school dashes + # i -> inverted old school dashes + # e -> ellipses + # w -> convert " entities to " for Dreamweaver users + # + # Spacing: + # : -> colon spacing +- + # ; -> semicolon spacing +- + # m -> question and exclamation marks spacing +- + # h -> em-dash spacing +- + # H -> en-dash spacing +- + # f -> french quote spacing +- + # t -> thousand separator spacing - + # u -> unit spacing +- + # (you can add a plus sign after some of these options denoted by + to + # add the space when it is not already present, or you can add a minus + # sign to completly remove any space present) + # + # Initialize inherited SmartyPants parser. + parent::__construct($attr); + + if ($attr == "1" || $attr == "2" || $attr == "3") { + # Do everything, turn all options on. + $this->do_comma_quotes = 1; + $this->do_guillemets = 1; + $this->do_geresh_gershayim = 1; + $this->do_space_emdash = 1; + $this->do_space_endash = 1; + $this->do_space_colon = 1; + $this->do_space_semicolon = 1; + $this->do_space_marks = 1; + $this->do_space_frenchquote = 1; + $this->do_space_thousand = 1; + $this->do_space_unit = 1; + } + else if ($attr == "-1") { + # Special "stupefy" mode. + $this->do_stupefy = 1; + } + else { + $chars = preg_split('//', $attr); + foreach ($chars as $c){ + if ($c == "c") { $current =& $this->do_comma_quotes; } + else if ($c == "g") { $current =& $this->do_guillemets; } + else if ($c == "G") { $current =& $this->do_geresh_gershayim; } + else if ($c == ":") { $current =& $this->do_space_colon; } + else if ($c == ";") { $current =& $this->do_space_semicolon; } + else if ($c == "m") { $current =& $this->do_space_marks; } + else if ($c == "h") { $current =& $this->do_space_emdash; } + else if ($c == "H") { $current =& $this->do_space_endash; } + else if ($c == "f") { $current =& $this->do_space_frenchquote; } + else if ($c == "t") { $current =& $this->do_space_thousand; } + else if ($c == "u") { $current =& $this->do_space_unit; } + else if ($c == "+") { + $current = 2; + unset($current); + } + else if ($c == "-") { + $current = -1; + unset($current); + } + else { + # Unknown attribute option, ignore. + } + $current = 1; + } + } + } + + + function decodeEntitiesInConfiguration() { + parent::decodeEntitiesInConfiguration(); + $output_config_vars = array( + 'doublequote_low', + 'guillemet_leftpointing', + 'guillemet_rightpointing', + 'space_emdash', + 'space_endash', + 'space_colon', + 'space_semicolon', + 'space_marks', + 'space_frenchquote', + 'space_thousand', + 'space_unit', + ); + foreach ($output_config_vars as $var) { + $this->$var = html_entity_decode($this->$var); + } + } + + + function educate($t, $prev_token_last_char) { + # must happen before regular smart quotes + if ($this->do_geresh_gershayim) $t = $this->educateGereshGershayim($t); + + $t = parent::educate($t, $prev_token_last_char); + + if ($this->do_comma_quotes) $t = $this->educateCommaQuotes($t); + if ($this->do_guillemets) $t = $this->educateGuillemets($t); + + if ($this->do_space_emdash) $t = $this->spaceEmDash($t); + if ($this->do_space_endash) $t = $this->spaceEnDash($t); + if ($this->do_space_colon) $t = $this->spaceColon($t); + if ($this->do_space_semicolon) $t = $this->spaceSemicolon($t); + if ($this->do_space_marks) $t = $this->spaceMarks($t); + if ($this->do_space_frenchquote) $t = $this->spaceFrenchQuotes($t); + if ($this->do_space_thousand) $t = $this->spaceThousandSeparator($t); + if ($this->do_space_unit) $t = $this->spaceUnit($t); + + return $t; + } + + + protected function educateCommaQuotes($_) { + # + # Parameter: String. + # Returns: The string, with ,,comma,, -style double quotes + # translated into HTML curly quote entities. + # + # Example input: ,,Isn't this fun?,, + # Example output: „Isn't this fun?„ + # + # Note: this is meant to be used alongside with backtick quotes; there is + # no language that use only lower quotations alone mark like in the example. + # + $_ = str_replace(",,", $this->doublequote_low, $_); + return $_; + } + + + protected function educateGuillemets($_) { + # + # Parameter: String. + # Returns: The string, with << guillemets >> -style quotes + # translated into HTML guillemets entities. + # + # Example input: << Isn't this fun? >> + # Example output: „ Isn't this fun? „ + # + $_ = preg_replace("/(?:<|<){2}/", $this->guillemet_leftpointing, $_); + $_ = preg_replace("/(?:>|>){2}/", $this->guillemet_rightpointing, $_); + return $_; + } + + + protected function educateGereshGershayim($_) { + # + # Parameter: String, UTF-8 encoded. + # Returns: The string, where simple a or double quote surrounded by + # two hebrew characters is replaced into a typographic + # geresh or gershayim punctuation mark. + # + # Example input: צה"ל / צ'ארלס + # Example output: צה״ל / צ׳ארלס + # + // surrounding code points can be U+0590 to U+05BF and U+05D0 to U+05F2 + // encoded in UTF-8: D6.90 to D6.BF and D7.90 to D7.B2 + $_ = preg_replace('/(?<=\xD6[\x90-\xBF]|\xD7[\x90-\xB2])\'(?=\xD6[\x90-\xBF]|\xD7[\x90-\xB2])/', $this->geresh, $_); + $_ = preg_replace('/(?<=\xD6[\x90-\xBF]|\xD7[\x90-\xB2])"(?=\xD6[\x90-\xBF]|\xD7[\x90-\xB2])/', $this->gershayim, $_); + return $_; + } + + + protected function spaceFrenchQuotes($_) { + # + # Parameters: String, replacement character, and forcing flag. + # Returns: The string, with appropriates spaces replaced + # inside french-style quotes, only french quotes. + # + # Example input: Quotes in « French », »German« and »Finnish» style. + # Example output: Quotes in «_French_», »German« and »Finnish» style. + # + $opt = ( $this->do_space_frenchquote == 2 ? '?' : '' ); + $chr = ( $this->do_space_frenchquote != -1 ? $this->space_frenchquote : '' ); + + # Characters allowed immediatly outside quotes. + $outside_char = $this->space . '|\s|[.,:;!?\[\](){}|@*~=+-]|¡|¿'; + + $_ = preg_replace( + "/(^|$outside_char)(«|«|›|‹)$this->space$opt/", + "\\1\\2$chr", $_); + $_ = preg_replace( + "/$this->space$opt(»|»|‹|›)($outside_char|$)/", + "$chr\\1\\2", $_); + return $_; + } + + + protected function spaceColon($_) { + # + # Parameters: String, replacement character, and forcing flag. + # Returns: The string, with appropriates spaces replaced + # before colons. + # + # Example input: Ingredients : fun. + # Example output: Ingredients_: fun. + # + $opt = ( $this->do_space_colon == 2 ? '?' : '' ); + $chr = ( $this->do_space_colon != -1 ? $this->space_colon : '' ); + + $_ = preg_replace("/$this->space$opt(:)(\\s|$)/m", + "$chr\\1\\2", $_); + return $_; + } + + + protected function spaceSemicolon($_) { + # + # Parameters: String, replacement character, and forcing flag. + # Returns: The string, with appropriates spaces replaced + # before semicolons. + # + # Example input: There he goes ; there she goes. + # Example output: There he goes_; there she goes. + # + $opt = ( $this->do_space_semicolon == 2 ? '?' : '' ); + $chr = ( $this->do_space_semicolon != -1 ? $this->space_semicolon : '' ); + + $_ = preg_replace("/$this->space(;)(?=\\s|$)/m", + " \\1", $_); + $_ = preg_replace("/((?:^|\\s)(?>[^&;\\s]+|&#?[a-zA-Z0-9]+;)*)". + " $opt(;)(?=\\s|$)/m", + "\\1$chr\\2", $_); + return $_; + } + + + protected function spaceMarks($_) { + # + # Parameters: String, replacement character, and forcing flag. + # Returns: The string, with appropriates spaces replaced + # around question and exclamation marks. + # + # Example input: ¡ Holà ! What ? + # Example output: ¡_Holà_! What_? + # + $opt = ( $this->do_space_marks == 2 ? '?' : '' ); + $chr = ( $this->do_space_marks != -1 ? $this->space_marks : '' ); + + // Regular marks. + $_ = preg_replace("/$this->space$opt([?!]+)/", "$chr\\1", $_); + + // Inverted marks. + $imarks = "(?:¡|¡|¡|&#x[Aa]1;|¿|¿|¿|&#x[Bb][Ff];)"; + $_ = preg_replace("/($imarks+)$this->space$opt/", "\\1$chr", $_); + + return $_; + } + + + protected function spaceEmDash($_) { + # + # Parameters: String, two replacement characters separated by a hyphen (`-`), + # and forcing flag. + # + # Returns: The string, with appropriates spaces replaced + # around dashes. + # + # Example input: Then — without any plan — the fun happend. + # Example output: Then_—_without any plan_—_the fun happend. + # + $opt = ( $this->do_space_emdash == 2 ? '?' : '' ); + $chr = ( $this->do_space_emdash != -1 ? $this->space_emdash : '' ); + $_ = preg_replace("/$this->space$opt(—|—)$this->space$opt/", + "$chr\\1$chr", $_); + return $_; + } + + + protected function spaceEnDash($_) { + # + # Parameters: String, two replacement characters separated by a hyphen (`-`), + # and forcing flag. + # + # Returns: The string, with appropriates spaces replaced + # around dashes. + # + # Example input: Then — without any plan — the fun happend. + # Example output: Then_—_without any plan_—_the fun happend. + # + $opt = ( $this->do_space_endash == 2 ? '?' : '' ); + $chr = ( $this->do_space_endash != -1 ? $this->space_endash : '' ); + $_ = preg_replace("/$this->space$opt(–|–)$this->space$opt/", + "$chr\\1$chr", $_); + return $_; + } + + + protected function spaceThousandSeparator($_) { + # + # Parameters: String, replacement character, and forcing flag. + # Returns: The string, with appropriates spaces replaced + # inside numbers (thousand separator in french). + # + # Example input: Il y a 10 000 insectes amusants dans ton jardin. + # Example output: Il y a 10_000 insectes amusants dans ton jardin. + # + $chr = ( $this->do_space_thousand != -1 ? $this->space_thousand : '' ); + $_ = preg_replace('/([0-9]) ([0-9])/', "\\1$chr\\2", $_); + return $_; + } + + + protected $units = ' + ### Metric units (with prefixes) + (?: + p | + µ | µ | &\#0*181; | &\#[xX]0*[Bb]5; | + [mcdhkMGT] + )? + (?: + [mgstAKNJWCVFSTHBL]|mol|cd|rad|Hz|Pa|Wb|lm|lx|Bq|Gy|Sv|kat| + Ω | Ohm | Ω | &\#0*937; | &\#[xX]0*3[Aa]9; + )| + ### Computers units (KB, Kb, TB, Kbps) + [kKMGT]?(?:[oBb]|[oBb]ps|flops)| + ### Money + ¢ | ¢ | &\#0*162; | &\#[xX]0*[Aa]2; | + M?(?: + £ | £ | &\#0*163; | &\#[xX]0*[Aa]3; | + ¥ | ¥ | &\#0*165; | &\#[xX]0*[Aa]5; | + € | € | &\#0*8364; | &\#[xX]0*20[Aa][Cc]; | + $ + )| + ### Other units + (?: ° | ° | &\#0*176; | &\#[xX]0*[Bb]0; ) [CF]? | + %|pt|pi|M?px|em|en|gal|lb|[NSEOW]|[NS][EOW]|ha|mbar + '; //x + + protected function spaceUnit($_) { + # + # Parameters: String, replacement character, and forcing flag. + # Returns: The string, with appropriates spaces replaced + # before unit symbols. + # + # Example input: Get 3 mol of fun for 3 $. + # Example output: Get 3_mol of fun for 3_$. + # + $opt = ( $this->do_space_unit == 2 ? '?' : '' ); + $chr = ( $this->do_space_unit != -1 ? $this->space_unit : '' ); + + $_ = preg_replace('/ + (?:([0-9])[ ]'.$opt.') # Number followed by space. + ('.$this->units.') # Unit. + (?![a-zA-Z0-9]) # Negative lookahead for other unit characters. + /x', + "\\1$chr\\2", $_); + + return $_; + } + + + protected function spaceAbbr($_) { + # + # Parameters: String, replacement character, and forcing flag. + # Returns: The string, with appropriates spaces replaced + # around abbreviations. + # + # Example input: Fun i.e. something pleasant. + # Example output: Fun i.e._something pleasant. + # + $opt = ( $this->do_space_abbr == 2 ? '?' : '' ); + + $_ = preg_replace("/(^|\s)($this->abbr_after) $opt/m", + "\\1\\2$this->space_abbr", $_); + $_ = preg_replace("/( )$opt($this->abbr_sp_before)(?![a-zA-Z'])/m", + "\\1$this->space_abbr\\2", $_); + return $_; + } + + + protected function stupefyEntities($_) { + # + # Adding angle quotes and lower quotes to SmartyPants's stupefy mode. + # + $_ = parent::stupefyEntities($_); + + $_ = str_replace(array('„', '«', '»'), '"', $_); + + return $_; + } + + + protected function processEscapes($_) { + # + # Adding a few more escapes to SmartyPants's escapes: + # + # Escape Value + # ------ ----- + # \, , + # \< < + # \> > + # + $_ = parent::processEscapes($_); + + $_ = str_replace( + array('\,', '\<', '\>', '\<', '\>'), + array(',', '<', '>', '<', '>'), $_); + + return $_; + } +} diff --git a/kirby/vendor/mustangostang/spyc/Spyc.php b/kirby/vendor/mustangostang/spyc/Spyc.php new file mode 100755 index 0000000..49e0cbb --- /dev/null +++ b/kirby/vendor/mustangostang/spyc/Spyc.php @@ -0,0 +1,1161 @@ + + * @author Chris Wanstrath + * @link https://github.com/mustangostang/spyc/ + * @copyright Copyright 2005-2006 Chris Wanstrath, 2006-2011 Vlad Andersen + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @package Spyc + */ + +if (!function_exists('spyc_load')) { + /** + * Parses YAML to array. + * @param string $string YAML string. + * @return array + */ + function spyc_load ($string) { + return Spyc::YAMLLoadString($string); + } +} + +if (!function_exists('spyc_load_file')) { + /** + * Parses YAML to array. + * @param string $file Path to YAML file. + * @return array + */ + function spyc_load_file ($file) { + return Spyc::YAMLLoad($file); + } +} + +if (!function_exists('spyc_dump')) { + /** + * Dumps array to YAML. + * @param array $data Array. + * @return string + */ + function spyc_dump ($data) { + return Spyc::YAMLDump($data, false, false, true); + } +} + +if (!class_exists('Spyc')) { + +/** + * The Simple PHP YAML Class. + * + * This class can be used to read a YAML file and convert its contents + * into a PHP array. It currently supports a very limited subsection of + * the YAML spec. + * + * Usage: + * + * $Spyc = new Spyc; + * $array = $Spyc->load($file); + * + * or: + * + * $array = Spyc::YAMLLoad($file); + * + * or: + * + * $array = spyc_load_file($file); + * + * @package Spyc + */ +class Spyc { + + // SETTINGS + + const REMPTY = "\0\0\0\0\0"; + + /** + * Setting this to true will force YAMLDump to enclose any string value in + * quotes. False by default. + * + * @var bool + */ + public $setting_dump_force_quotes = false; + + /** + * Setting this to true will forse YAMLLoad to use syck_load function when + * possible. False by default. + * @var bool + */ + public $setting_use_syck_is_possible = false; + + + + /**#@+ + * @access private + * @var mixed + */ + private $_dumpIndent; + private $_dumpWordWrap; + private $_containsGroupAnchor = false; + private $_containsGroupAlias = false; + private $path; + private $result; + private $LiteralPlaceHolder = '___YAML_Literal_Block___'; + private $SavedGroups = array(); + private $indent; + /** + * Path modifier that should be applied after adding current element. + * @var array + */ + private $delayedPath = array(); + + /**#@+ + * @access public + * @var mixed + */ + public $_nodeId; + +/** + * Load a valid YAML string to Spyc. + * @param string $input + * @return array + */ + public function load ($input) { + return $this->_loadString($input); + } + + /** + * Load a valid YAML file to Spyc. + * @param string $file + * @return array + */ + public function loadFile ($file) { + return $this->_load($file); + } + + /** + * Load YAML into a PHP array statically + * + * The load method, when supplied with a YAML stream (string or file), + * will do its best to convert YAML in a file into a PHP array. Pretty + * simple. + * Usage: + * + * $array = Spyc::YAMLLoad('lucky.yaml'); + * print_r($array); + * + * @access public + * @return array + * @param string $input Path of YAML file or string containing YAML + */ + public static function YAMLLoad($input) { + $Spyc = new Spyc; + return $Spyc->_load($input); + } + + /** + * Load a string of YAML into a PHP array statically + * + * The load method, when supplied with a YAML string, will do its best + * to convert YAML in a string into a PHP array. Pretty simple. + * + * Note: use this function if you don't want files from the file system + * loaded and processed as YAML. This is of interest to people concerned + * about security whose input is from a string. + * + * Usage: + * + * $array = Spyc::YAMLLoadString("---\n0: hello world\n"); + * print_r($array); + * + * @access public + * @return array + * @param string $input String containing YAML + */ + public static function YAMLLoadString($input) { + $Spyc = new Spyc; + return $Spyc->_loadString($input); + } + + /** + * Dump YAML from PHP array statically + * + * The dump method, when supplied with an array, will do its best + * to convert the array into friendly YAML. Pretty simple. Feel free to + * save the returned string as nothing.yaml and pass it around. + * + * Oh, and you can decide how big the indent is and what the wordwrap + * for folding is. Pretty cool -- just pass in 'false' for either if + * you want to use the default. + * + * Indent's default is 2 spaces, wordwrap's default is 40 characters. And + * you can turn off wordwrap by passing in 0. + * + * @access public + * @return string + * @param array|\stdClass $array PHP array + * @param int $indent Pass in false to use the default, which is 2 + * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) + * @param bool $no_opening_dashes Do not start YAML file with "---\n" + */ + public static function YAMLDump($array, $indent = false, $wordwrap = false, $no_opening_dashes = false) { + $spyc = new Spyc; + return $spyc->dump($array, $indent, $wordwrap, $no_opening_dashes); + } + + + /** + * Dump PHP array to YAML + * + * The dump method, when supplied with an array, will do its best + * to convert the array into friendly YAML. Pretty simple. Feel free to + * save the returned string as tasteful.yaml and pass it around. + * + * Oh, and you can decide how big the indent is and what the wordwrap + * for folding is. Pretty cool -- just pass in 'false' for either if + * you want to use the default. + * + * Indent's default is 2 spaces, wordwrap's default is 40 characters. And + * you can turn off wordwrap by passing in 0. + * + * @access public + * @return string + * @param array $array PHP array + * @param int $indent Pass in false to use the default, which is 2 + * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) + */ + public function dump($array,$indent = false,$wordwrap = false, $no_opening_dashes = false) { + // Dumps to some very clean YAML. We'll have to add some more features + // and options soon. And better support for folding. + + // New features and options. + if ($indent === false or !is_numeric($indent)) { + $this->_dumpIndent = 2; + } else { + $this->_dumpIndent = $indent; + } + + if ($wordwrap === false or !is_numeric($wordwrap)) { + $this->_dumpWordWrap = 40; + } else { + $this->_dumpWordWrap = $wordwrap; + } + + // New YAML document + $string = ""; + if (!$no_opening_dashes) $string = "---\n"; + + // Start at the base of the array and move through it. + if ($array) { + $array = (array)$array; + $previous_key = -1; + foreach ($array as $key => $value) { + if (!isset($first_key)) $first_key = $key; + $string .= $this->_yamlize($key,$value,0,$previous_key, $first_key, $array); + $previous_key = $key; + } + } + return $string; + } + + /** + * Attempts to convert a key / value array item to YAML + * @access private + * @return string + * @param $key The name of the key + * @param $value The value of the item + * @param $indent The indent of the current node + */ + private function _yamlize($key,$value,$indent, $previous_key = -1, $first_key = 0, $source_array = null) { + if(is_object($value)) $value = (array)$value; + if (is_array($value)) { + if (empty ($value)) + return $this->_dumpNode($key, array(), $indent, $previous_key, $first_key, $source_array); + // It has children. What to do? + // Make it the right kind of item + $string = $this->_dumpNode($key, self::REMPTY, $indent, $previous_key, $first_key, $source_array); + // Add the indent + $indent += $this->_dumpIndent; + // Yamlize the array + $string .= $this->_yamlizeArray($value,$indent); + } elseif (!is_array($value)) { + // It doesn't have children. Yip. + $string = $this->_dumpNode($key, $value, $indent, $previous_key, $first_key, $source_array); + } + return $string; + } + + /** + * Attempts to convert an array to YAML + * @access private + * @return string + * @param $array The array you want to convert + * @param $indent The indent of the current level + */ + private function _yamlizeArray($array,$indent) { + if (is_array($array)) { + $string = ''; + $previous_key = -1; + foreach ($array as $key => $value) { + if (!isset($first_key)) $first_key = $key; + $string .= $this->_yamlize($key, $value, $indent, $previous_key, $first_key, $array); + $previous_key = $key; + } + return $string; + } else { + return false; + } + } + + /** + * Returns YAML from a key and a value + * @access private + * @return string + * @param $key The name of the key + * @param $value The value of the item + * @param $indent The indent of the current node + */ + private function _dumpNode($key, $value, $indent, $previous_key = -1, $first_key = 0, $source_array = null) { + // do some folding here, for blocks + if (is_string ($value) && ((strpos($value,"\n") !== false || strpos($value,": ") !== false || strpos($value,"- ") !== false || + strpos($value,"*") !== false || strpos($value,"#") !== false || strpos($value,"<") !== false || strpos($value,">") !== false || strpos ($value, '%') !== false || strpos ($value, ' ') !== false || + strpos($value,"[") !== false || strpos($value,"]") !== false || strpos($value,"{") !== false || strpos($value,"}") !== false) || strpos($value,"&") !== false || strpos($value, "'") !== false || strpos($value, "!") === 0 || + substr ($value, -1, 1) == ':') + ) { + $value = $this->_doLiteralBlock($value,$indent); + } else { + $value = $this->_doFolding($value,$indent); + } + + if ($value === array()) $value = '[ ]'; + if ($value === "") $value = '""'; + if (self::isTranslationWord($value)) { + $value = $this->_doLiteralBlock($value, $indent); + } + if (trim ($value) != $value) + $value = $this->_doLiteralBlock($value,$indent); + + if (is_bool($value)) { + $value = $value ? "true" : "false"; + } + + if ($value === null) $value = 'null'; + if ($value === "'" . self::REMPTY . "'") $value = null; + + $spaces = str_repeat(' ',$indent); + + //if (is_int($key) && $key - 1 == $previous_key && $first_key===0) { + if (is_array ($source_array) && array_keys($source_array) === range(0, count($source_array) - 1)) { + // It's a sequence + $string = $spaces.'- '.$value."\n"; + } else { + // if ($first_key===0) throw new Exception('Keys are all screwy. The first one was zero, now it\'s "'. $key .'"'); + // It's mapped + if (strpos($key, ":") !== false || strpos($key, "#") !== false) { $key = '"' . $key . '"'; } + $string = rtrim ($spaces.$key.': '.$value)."\n"; + } + return $string; + } + + /** + * Creates a literal block for dumping + * @access private + * @return string + * @param $value + * @param $indent int The value of the indent + */ + private function _doLiteralBlock($value,$indent) { + if ($value === "\n") return '\n'; + if (strpos($value, "\n") === false && strpos($value, "'") === false) { + return sprintf ("'%s'", $value); + } + if (strpos($value, "\n") === false && strpos($value, '"') === false) { + return sprintf ('"%s"', $value); + } + $exploded = explode("\n",$value); + $newValue = '|'; + if (isset($exploded[0]) && ($exploded[0] == "|" || $exploded[0] == "|-" || $exploded[0] == ">")) { + $newValue = $exploded[0]; + unset($exploded[0]); + } + $indent += $this->_dumpIndent; + $spaces = str_repeat(' ',$indent); + foreach ($exploded as $line) { + $line = trim($line); + if (strpos($line, '"') === 0 && strrpos($line, '"') == (strlen($line)-1) || strpos($line, "'") === 0 && strrpos($line, "'") == (strlen($line)-1)) { + $line = substr($line, 1, -1); + } + $newValue .= "\n" . $spaces . ($line); + } + return $newValue; + } + + /** + * Folds a string of text, if necessary + * @access private + * @return string + * @param $value The string you wish to fold + */ + private function _doFolding($value,$indent) { + // Don't do anything if wordwrap is set to 0 + + if ($this->_dumpWordWrap !== 0 && is_string ($value) && strlen($value) > $this->_dumpWordWrap) { + $indent += $this->_dumpIndent; + $indent = str_repeat(' ',$indent); + $wrapped = wordwrap($value,$this->_dumpWordWrap,"\n$indent"); + $value = ">\n".$indent.$wrapped; + } else { + if ($this->setting_dump_force_quotes && is_string ($value) && $value !== self::REMPTY) + $value = '"' . $value . '"'; + if (is_numeric($value) && is_string($value)) + $value = '"' . $value . '"'; + } + + + return $value; + } + + private function isTrueWord($value) { + $words = self::getTranslations(array('true', 'on', 'yes', 'y')); + return in_array($value, $words, true); + } + + private function isFalseWord($value) { + $words = self::getTranslations(array('false', 'off', 'no', 'n')); + return in_array($value, $words, true); + } + + private function isNullWord($value) { + $words = self::getTranslations(array('null', '~')); + return in_array($value, $words, true); + } + + private function isTranslationWord($value) { + return ( + self::isTrueWord($value) || + self::isFalseWord($value) || + self::isNullWord($value) + ); + } + + /** + * Coerce a string into a native type + * Reference: http://yaml.org/type/bool.html + * TODO: Use only words from the YAML spec. + * @access private + * @param $value The value to coerce + */ + private function coerceValue(&$value) { + if (self::isTrueWord($value)) { + $value = true; + } else if (self::isFalseWord($value)) { + $value = false; + } else if (self::isNullWord($value)) { + $value = null; + } + } + + /** + * Given a set of words, perform the appropriate translations on them to + * match the YAML 1.1 specification for type coercing. + * @param $words The words to translate + * @access private + */ + private static function getTranslations(array $words) { + $result = array(); + foreach ($words as $i) { + $result = array_merge($result, array(ucfirst($i), strtoupper($i), strtolower($i))); + } + return $result; + } + +// LOADING FUNCTIONS + + private function _load($input) { + $Source = $this->loadFromSource($input); + return $this->loadWithSource($Source); + } + + private function _loadString($input) { + $Source = $this->loadFromString($input); + return $this->loadWithSource($Source); + } + + private function loadWithSource($Source) { + if (empty ($Source)) return array(); + if ($this->setting_use_syck_is_possible && function_exists ('syck_load')) { + $array = syck_load (implode ("\n", $Source)); + return is_array($array) ? $array : array(); + } + + $this->path = array(); + $this->result = array(); + + $cnt = count($Source); + for ($i = 0; $i < $cnt; $i++) { + $line = $Source[$i]; + + $this->indent = strlen($line) - strlen(ltrim($line)); + $tempPath = $this->getParentPathByIndent($this->indent); + $line = self::stripIndent($line, $this->indent); + if (self::isComment($line)) continue; + if (self::isEmpty($line)) continue; + $this->path = $tempPath; + + $literalBlockStyle = self::startsLiteralBlock($line); + if ($literalBlockStyle) { + $line = rtrim ($line, $literalBlockStyle . " \n"); + $literalBlock = ''; + $line .= ' '.$this->LiteralPlaceHolder; + $literal_block_indent = strlen($Source[$i+1]) - strlen(ltrim($Source[$i+1])); + while (++$i < $cnt && $this->literalBlockContinues($Source[$i], $this->indent)) { + $literalBlock = $this->addLiteralLine($literalBlock, $Source[$i], $literalBlockStyle, $literal_block_indent); + } + $i--; + } + + // Strip out comments + if (strpos ($line, '#')) { + $line = preg_replace('/\s*#([^"\']+)$/','',$line); + } + + while (++$i < $cnt && self::greedilyNeedNextLine($line)) { + $line = rtrim ($line, " \n\t\r") . ' ' . ltrim ($Source[$i], " \t"); + } + $i--; + + $lineArray = $this->_parseLine($line); + + if ($literalBlockStyle) + $lineArray = $this->revertLiteralPlaceHolder ($lineArray, $literalBlock); + + $this->addArray($lineArray, $this->indent); + + foreach ($this->delayedPath as $indent => $delayedPath) + $this->path[$indent] = $delayedPath; + + $this->delayedPath = array(); + + } + return $this->result; + } + + private function loadFromSource ($input) { + if (!empty($input) && strpos($input, "\n") === false && file_exists($input)) + $input = file_get_contents($input); + + return $this->loadFromString($input); + } + + private function loadFromString ($input) { + $lines = explode("\n",$input); + foreach ($lines as $k => $_) { + $lines[$k] = rtrim ($_, "\r"); + } + return $lines; + } + + /** + * Parses YAML code and returns an array for a node + * @access private + * @return array + * @param string $line A line from the YAML file + */ + private function _parseLine($line) { + if (!$line) return array(); + $line = trim($line); + if (!$line) return array(); + + $array = array(); + + $group = $this->nodeContainsGroup($line); + if ($group) { + $this->addGroup($line, $group); + $line = $this->stripGroup ($line, $group); + } + + if ($this->startsMappedSequence($line)) + return $this->returnMappedSequence($line); + + if ($this->startsMappedValue($line)) + return $this->returnMappedValue($line); + + if ($this->isArrayElement($line)) + return $this->returnArrayElement($line); + + if ($this->isPlainArray($line)) + return $this->returnPlainArray($line); + + + return $this->returnKeyValuePair($line); + + } + + /** + * Finds the type of the passed value, returns the value as the new type. + * @access private + * @param string $value + * @return mixed + */ + private function _toType($value) { + if ($value === '') return ""; + $first_character = $value[0]; + $last_character = substr($value, -1, 1); + + $is_quoted = false; + do { + if (!$value) break; + if ($first_character != '"' && $first_character != "'") break; + if ($last_character != '"' && $last_character != "'") break; + $is_quoted = true; + } while (0); + + if ($is_quoted) { + $value = str_replace('\n', "\n", $value); + if ($first_character == "'") + return strtr(substr ($value, 1, -1), array ('\'\'' => '\'', '\\\''=> '\'')); + return strtr(substr ($value, 1, -1), array ('\\"' => '"', '\\\''=> '\'')); + } + + if (strpos($value, ' #') !== false && !$is_quoted) + $value = preg_replace('/\s+#(.+)$/','',$value); + + if ($first_character == '[' && $last_character == ']') { + // Take out strings sequences and mappings + $innerValue = trim(substr ($value, 1, -1)); + if ($innerValue === '') return array(); + $explode = $this->_inlineEscape($innerValue); + // Propagate value array + $value = array(); + foreach ($explode as $v) { + $value[] = $this->_toType($v); + } + return $value; + } + + if (strpos($value,': ')!==false && $first_character != '{') { + $array = explode(': ',$value); + $key = trim($array[0]); + array_shift($array); + $value = trim(implode(': ',$array)); + $value = $this->_toType($value); + return array($key => $value); + } + + if ($first_character == '{' && $last_character == '}') { + $innerValue = trim(substr ($value, 1, -1)); + if ($innerValue === '') return array(); + // Inline Mapping + // Take out strings sequences and mappings + $explode = $this->_inlineEscape($innerValue); + // Propagate value array + $array = array(); + foreach ($explode as $v) { + $SubArr = $this->_toType($v); + if (empty($SubArr)) continue; + if (is_array ($SubArr)) { + $array[key($SubArr)] = $SubArr[key($SubArr)]; continue; + } + $array[] = $SubArr; + } + return $array; + } + + if ($value == 'null' || $value == 'NULL' || $value == 'Null' || $value == '' || $value == '~') { + return null; + } + + if ( is_numeric($value) && preg_match ('/^(-|)[1-9]+[0-9]*$/', $value) ){ + $intvalue = (int)$value; + if ($intvalue != PHP_INT_MAX && $intvalue != ~PHP_INT_MAX) + $value = $intvalue; + return $value; + } + + if ( is_string($value) && preg_match('/^0[xX][0-9a-fA-F]+$/', $value)) { + // Hexadecimal value. + return hexdec($value); + } + + $this->coerceValue($value); + + if (is_numeric($value)) { + if ($value === '0') return 0; + if (rtrim ($value, 0) === $value) + $value = (float)$value; + return $value; + } + + return $value; + } + + /** + * Used in inlines to check for more inlines or quoted strings + * @access private + * @return array + */ + private function _inlineEscape($inline) { + // There's gotta be a cleaner way to do this... + // While pure sequences seem to be nesting just fine, + // pure mappings and mappings with sequences inside can't go very + // deep. This needs to be fixed. + + $seqs = array(); + $maps = array(); + $saved_strings = array(); + $saved_empties = array(); + + // Check for empty strings + $regex = '/("")|(\'\')/'; + if (preg_match_all($regex,$inline,$strings)) { + $saved_empties = $strings[0]; + $inline = preg_replace($regex,'YAMLEmpty',$inline); + } + unset($regex); + + // Check for strings + $regex = '/(?:(")|(?:\'))((?(1)[^"]+|[^\']+))(?(1)"|\')/'; + if (preg_match_all($regex,$inline,$strings)) { + $saved_strings = $strings[0]; + $inline = preg_replace($regex,'YAMLString',$inline); + } + unset($regex); + + // echo $inline; + + $i = 0; + do { + + // Check for sequences + while (preg_match('/\[([^{}\[\]]+)\]/U',$inline,$matchseqs)) { + $seqs[] = $matchseqs[0]; + $inline = preg_replace('/\[([^{}\[\]]+)\]/U', ('YAMLSeq' . (count($seqs) - 1) . 's'), $inline, 1); + } + + // Check for mappings + while (preg_match('/{([^\[\]{}]+)}/U',$inline,$matchmaps)) { + $maps[] = $matchmaps[0]; + $inline = preg_replace('/{([^\[\]{}]+)}/U', ('YAMLMap' . (count($maps) - 1) . 's'), $inline, 1); + } + + if ($i++ >= 10) break; + + } while (strpos ($inline, '[') !== false || strpos ($inline, '{') !== false); + + $explode = explode(',',$inline); + $explode = array_map('trim', $explode); + $stringi = 0; $i = 0; + + while (1) { + + // Re-add the sequences + if (!empty($seqs)) { + foreach ($explode as $key => $value) { + if (strpos($value,'YAMLSeq') !== false) { + foreach ($seqs as $seqk => $seq) { + $explode[$key] = str_replace(('YAMLSeq'.$seqk.'s'),$seq,$value); + $value = $explode[$key]; + } + } + } + } + + // Re-add the mappings + if (!empty($maps)) { + foreach ($explode as $key => $value) { + if (strpos($value,'YAMLMap') !== false) { + foreach ($maps as $mapk => $map) { + $explode[$key] = str_replace(('YAMLMap'.$mapk.'s'), $map, $value); + $value = $explode[$key]; + } + } + } + } + + + // Re-add the strings + if (!empty($saved_strings)) { + foreach ($explode as $key => $value) { + while (strpos($value,'YAMLString') !== false) { + $explode[$key] = preg_replace('/YAMLString/',$saved_strings[$stringi],$value, 1); + unset($saved_strings[$stringi]); + ++$stringi; + $value = $explode[$key]; + } + } + } + + + // Re-add the empties + if (!empty($saved_empties)) { + foreach ($explode as $key => $value) { + while (strpos($value,'YAMLEmpty') !== false) { + $explode[$key] = preg_replace('/YAMLEmpty/', '', $value, 1); + $value = $explode[$key]; + } + } + } + + $finished = true; + foreach ($explode as $key => $value) { + if (strpos($value,'YAMLSeq') !== false) { + $finished = false; break; + } + if (strpos($value,'YAMLMap') !== false) { + $finished = false; break; + } + if (strpos($value,'YAMLString') !== false) { + $finished = false; break; + } + if (strpos($value,'YAMLEmpty') !== false) { + $finished = false; break; + } + } + if ($finished) break; + + $i++; + if ($i > 10) + break; // Prevent infinite loops. + } + + + return $explode; + } + + private function literalBlockContinues ($line, $lineIndent) { + if (!trim($line)) return true; + if (strlen($line) - strlen(ltrim($line)) > $lineIndent) return true; + return false; + } + + private function referenceContentsByAlias ($alias) { + do { + if (!isset($this->SavedGroups[$alias])) { echo "Bad group name: $alias."; break; } + $groupPath = $this->SavedGroups[$alias]; + $value = $this->result; + foreach ($groupPath as $k) { + $value = $value[$k]; + } + } while (false); + return $value; + } + + private function addArrayInline ($array, $indent) { + $CommonGroupPath = $this->path; + if (empty ($array)) return false; + + foreach ($array as $k => $_) { + $this->addArray(array($k => $_), $indent); + $this->path = $CommonGroupPath; + } + return true; + } + + private function addArray ($incoming_data, $incoming_indent) { + + // print_r ($incoming_data); + + if (count ($incoming_data) > 1) + return $this->addArrayInline ($incoming_data, $incoming_indent); + + $key = key ($incoming_data); + $value = isset($incoming_data[$key]) ? $incoming_data[$key] : null; + if ($key === '__!YAMLZero') $key = '0'; + + if ($incoming_indent == 0 && !$this->_containsGroupAlias && !$this->_containsGroupAnchor) { // Shortcut for root-level values. + if ($key || $key === '' || $key === '0') { + $this->result[$key] = $value; + } else { + $this->result[] = $value; end ($this->result); $key = key ($this->result); + } + $this->path[$incoming_indent] = $key; + return; + } + + + + $history = array(); + // Unfolding inner array tree. + $history[] = $_arr = $this->result; + foreach ($this->path as $k) { + $history[] = $_arr = $_arr[$k]; + } + + if ($this->_containsGroupAlias) { + $value = $this->referenceContentsByAlias($this->_containsGroupAlias); + $this->_containsGroupAlias = false; + } + + + // Adding string or numeric key to the innermost level or $this->arr. + if (is_string($key) && $key == '<<') { + if (!is_array ($_arr)) { $_arr = array (); } + + $_arr = array_merge ($_arr, $value); + } else if ($key || $key === '' || $key === '0') { + if (!is_array ($_arr)) + $_arr = array ($key=>$value); + else + $_arr[$key] = $value; + } else { + if (!is_array ($_arr)) { $_arr = array ($value); $key = 0; } + else { $_arr[] = $value; end ($_arr); $key = key ($_arr); } + } + + $reverse_path = array_reverse($this->path); + $reverse_history = array_reverse ($history); + $reverse_history[0] = $_arr; + $cnt = count($reverse_history) - 1; + for ($i = 0; $i < $cnt; $i++) { + $reverse_history[$i+1][$reverse_path[$i]] = $reverse_history[$i]; + } + $this->result = $reverse_history[$cnt]; + + $this->path[$incoming_indent] = $key; + + if ($this->_containsGroupAnchor) { + $this->SavedGroups[$this->_containsGroupAnchor] = $this->path; + if (is_array ($value)) { + $k = key ($value); + if (!is_int ($k)) { + $this->SavedGroups[$this->_containsGroupAnchor][$incoming_indent + 2] = $k; + } + } + $this->_containsGroupAnchor = false; + } + + } + + private static function startsLiteralBlock ($line) { + $lastChar = substr (trim($line), -1); + if ($lastChar != '>' && $lastChar != '|') return false; + if ($lastChar == '|') return $lastChar; + // HTML tags should not be counted as literal blocks. + if (preg_match ('#<.*?>$#', $line)) return false; + return $lastChar; + } + + private static function greedilyNeedNextLine($line) { + $line = trim ($line); + if (!strlen($line)) return false; + if (substr ($line, -1, 1) == ']') return false; + if ($line[0] == '[') return true; + if (preg_match ('#^[^:]+?:\s*\[#', $line)) return true; + return false; + } + + private function addLiteralLine ($literalBlock, $line, $literalBlockStyle, $indent = -1) { + $line = self::stripIndent($line, $indent); + if ($literalBlockStyle !== '|') { + $line = self::stripIndent($line); + } + $line = rtrim ($line, "\r\n\t ") . "\n"; + if ($literalBlockStyle == '|') { + return $literalBlock . $line; + } + if (strlen($line) == 0) + return rtrim($literalBlock, ' ') . "\n"; + if ($line == "\n" && $literalBlockStyle == '>') { + return rtrim ($literalBlock, " \t") . "\n"; + } + if ($line != "\n") + $line = trim ($line, "\r\n ") . " "; + return $literalBlock . $line; + } + + function revertLiteralPlaceHolder ($lineArray, $literalBlock) { + foreach ($lineArray as $k => $_) { + if (is_array($_)) + $lineArray[$k] = $this->revertLiteralPlaceHolder ($_, $literalBlock); + else if (substr($_, -1 * strlen ($this->LiteralPlaceHolder)) == $this->LiteralPlaceHolder) + $lineArray[$k] = rtrim ($literalBlock, " \r\n"); + } + return $lineArray; + } + + private static function stripIndent ($line, $indent = -1) { + if ($indent == -1) $indent = strlen($line) - strlen(ltrim($line)); + return substr ($line, $indent); + } + + private function getParentPathByIndent ($indent) { + if ($indent == 0) return array(); + $linePath = $this->path; + do { + end($linePath); $lastIndentInParentPath = key($linePath); + if ($indent <= $lastIndentInParentPath) array_pop ($linePath); + } while ($indent <= $lastIndentInParentPath); + return $linePath; + } + + + private function clearBiggerPathValues ($indent) { + + + if ($indent == 0) $this->path = array(); + if (empty ($this->path)) return true; + + foreach ($this->path as $k => $_) { + if ($k > $indent) unset ($this->path[$k]); + } + + return true; + } + + + private static function isComment ($line) { + if (!$line) return false; + if ($line[0] == '#') return true; + if (trim($line, " \r\n\t") == '---') return true; + return false; + } + + private static function isEmpty ($line) { + return (trim ($line) === ''); + } + + + private function isArrayElement ($line) { + if (!$line || !is_scalar($line)) return false; + if (substr($line, 0, 2) != '- ') return false; + if (strlen ($line) > 3) + if (substr($line,0,3) == '---') return false; + + return true; + } + + private function isHashElement ($line) { + return strpos($line, ':'); + } + + private function isLiteral ($line) { + if ($this->isArrayElement($line)) return false; + if ($this->isHashElement($line)) return false; + return true; + } + + + private static function unquote ($value) { + if (!$value) return $value; + if (!is_string($value)) return $value; + if ($value[0] == '\'') return trim ($value, '\''); + if ($value[0] == '"') return trim ($value, '"'); + return $value; + } + + private function startsMappedSequence ($line) { + return (substr($line, 0, 2) == '- ' && substr ($line, -1, 1) == ':'); + } + + private function returnMappedSequence ($line) { + $array = array(); + $key = self::unquote(trim(substr($line,1,-1))); + $array[$key] = array(); + $this->delayedPath = array(strpos ($line, $key) + $this->indent => $key); + return array($array); + } + + private function checkKeysInValue($value) { + if (strchr('[{"\'', $value[0]) === false) { + if (strchr($value, ': ') !== false) { + throw new Exception('Too many keys: '.$value); + } + } + } + + private function returnMappedValue ($line) { + $this->checkKeysInValue($line); + $array = array(); + $key = self::unquote (trim(substr($line,0,-1))); + $array[$key] = ''; + return $array; + } + + private function startsMappedValue ($line) { + return (substr ($line, -1, 1) == ':'); + } + + private function isPlainArray ($line) { + return ($line[0] == '[' && substr ($line, -1, 1) == ']'); + } + + private function returnPlainArray ($line) { + return $this->_toType($line); + } + + private function returnKeyValuePair ($line) { + $array = array(); + $key = ''; + if (strpos ($line, ': ')) { + // It's a key/value pair most likely + // If the key is in double quotes pull it out + if (($line[0] == '"' || $line[0] == "'") && preg_match('/^(["\'](.*)["\'](\s)*:)/',$line,$matches)) { + $value = trim(str_replace($matches[1],'',$line)); + $key = $matches[2]; + } else { + // Do some guesswork as to the key and the value + $explode = explode(': ', $line); + $key = trim(array_shift($explode)); + $value = trim(implode(': ', $explode)); + $this->checkKeysInValue($value); + } + // Set the type of the value. Int, string, etc + $value = $this->_toType($value); + if ($key === '0') $key = '__!YAMLZero'; + $array[$key] = $value; + } else { + $array = array ($line); + } + return $array; + + } + + + private function returnArrayElement ($line) { + if (strlen($line) <= 1) return array(array()); // Weird %) + $array = array(); + $value = trim(substr($line,1)); + $value = $this->_toType($value); + if ($this->isArrayElement($value)) { + $value = $this->returnArrayElement($value); + } + $array[] = $value; + return $array; + } + + + private function nodeContainsGroup ($line) { + $symbolsForReference = 'A-z0-9_\-'; + if (strpos($line, '&') === false && strpos($line, '*') === false) return false; // Please die fast ;-) + if ($line[0] == '&' && preg_match('/^(&['.$symbolsForReference.']+)/', $line, $matches)) return $matches[1]; + if ($line[0] == '*' && preg_match('/^(\*['.$symbolsForReference.']+)/', $line, $matches)) return $matches[1]; + if (preg_match('/(&['.$symbolsForReference.']+)$/', $line, $matches)) return $matches[1]; + if (preg_match('/(\*['.$symbolsForReference.']+$)/', $line, $matches)) return $matches[1]; + if (preg_match ('#^\s*<<\s*:\s*(\*[^\s]+).*$#', $line, $matches)) return $matches[1]; + return false; + + } + + private function addGroup ($line, $group) { + if ($group[0] == '&') $this->_containsGroupAnchor = substr ($group, 1); + if ($group[0] == '*') $this->_containsGroupAlias = substr ($group, 1); + //print_r ($this->path); + } + + private function stripGroup ($line, $group) { + $line = trim(str_replace($group, '', $line)); + return $line; + } +} +} + +// Enable use of Spyc from command line +// The syntax is the following: php Spyc.php spyc.yaml + +do { + if (PHP_SAPI != 'cli') break; + if (empty ($_SERVER['argc']) || $_SERVER['argc'] < 2) break; + if (empty ($_SERVER['PHP_SELF']) || FALSE === strpos ($_SERVER['PHP_SELF'], 'Spyc.php') ) break; + $file = $argv[1]; + echo json_encode (spyc_load_file ($file)); +} while (0); diff --git a/kirby/vendor/phpmailer/phpmailer/get_oauth_token.php b/kirby/vendor/phpmailer/phpmailer/get_oauth_token.php new file mode 100755 index 0000000..1237b57 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/get_oauth_token.php @@ -0,0 +1,144 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2017 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ +/** + * Get an OAuth2 token from an OAuth2 provider. + * * Install this script on your server so that it's accessible + * as [https/http]:////get_oauth_token.php + * e.g.: http://localhost/phpmailer/get_oauth_token.php + * * Ensure dependencies are installed with 'composer install' + * * Set up an app in your Google/Yahoo/Microsoft account + * * Set the script address as the app's redirect URL + * If no refresh token is obtained when running this file, + * revoke access to your app and run the script again. + */ + +namespace PHPMailer\PHPMailer; + +/** + * Aliases for League Provider Classes + * Make sure you have added these to your composer.json and run `composer install` + * Plenty to choose from here: + * @see http://oauth2-client.thephpleague.com/providers/thirdparty/ + */ +// @see https://github.com/thephpleague/oauth2-google +use League\OAuth2\Client\Provider\Google; +// @see https://packagist.org/packages/hayageek/oauth2-yahoo +use Hayageek\OAuth2\Client\Provider\Yahoo; +// @see https://github.com/stevenmaguire/oauth2-microsoft +use Stevenmaguire\OAuth2\Client\Provider\Microsoft; + +if (!isset($_GET['code']) && !isset($_GET['provider'])) { +?> + +Select Provider:
+
Google
+Yahoo
+Microsoft/Outlook/Hotmail/Live/Office365
+ + + $clientId, + 'clientSecret' => $clientSecret, + 'redirectUri' => $redirectUri, + 'accessType' => 'offline' +]; + +$options = []; +$provider = null; + +switch ($providerName) { + case 'Google': + $provider = new Google($params); + $options = [ + 'scope' => [ + 'https://mail.google.com/' + ] + ]; + break; + case 'Yahoo': + $provider = new Yahoo($params); + break; + case 'Microsoft': + $provider = new Microsoft($params); + $options = [ + 'scope' => [ + 'wl.imap', + 'wl.offline_access' + ] + ]; + break; +} + +if (null === $provider) { + exit('Provider missing'); +} + +if (!isset($_GET['code'])) { + // If we don't have an authorization code then get one + $authUrl = $provider->getAuthorizationUrl($options); + $_SESSION['oauth2state'] = $provider->getState(); + header('Location: ' . $authUrl); + exit; +// Check given state against previously stored one to mitigate CSRF attack +} elseif (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) { + unset($_SESSION['oauth2state']); + unset($_SESSION['provider']); + exit('Invalid state'); +} else { + unset($_SESSION['provider']); + // Try to get an access token (using the authorization code grant) + $token = $provider->getAccessToken( + 'authorization_code', + [ + 'code' => $_GET['code'] + ] + ); + // Use this to interact with an API on the users behalf + // Use this to get a new access token if the old one expires + echo 'Refresh Token: ', $token->getRefreshToken(); +} diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-am.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-am.php new file mode 100755 index 0000000..ff2a969 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-am.php @@ -0,0 +1,26 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP -ի սխալ: չհաջողվեց ստուգել իսկությունը.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP -ի սխալ: չհաջողվեց կապ հաստատել SMTP սերվերի հետ.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP -ի սխալ: տվյալները ընդունված չեն.'; +$PHPMAILER_LANG['empty_message'] = 'Հաղորդագրությունը դատարկ է'; +$PHPMAILER_LANG['encoding'] = 'Կոդավորման անհայտ տեսակ: '; +$PHPMAILER_LANG['execute'] = 'Չհաջողվեց իրականացնել հրամանը: '; +$PHPMAILER_LANG['file_access'] = 'Ֆայլը հասանելի չէ: '; +$PHPMAILER_LANG['file_open'] = 'Ֆայլի սխալ: ֆայլը չհաջողվեց բացել: '; +$PHPMAILER_LANG['from_failed'] = 'Ուղարկողի հետևյալ հասցեն սխալ է: '; +$PHPMAILER_LANG['instantiate'] = 'Հնարավոր չէ կանչել mail ֆունկցիան.'; +$PHPMAILER_LANG['invalid_address'] = 'Հասցեն սխալ է: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' փոստային սերվերի հետ չի աշխատում.'; +$PHPMAILER_LANG['provide_address'] = 'Անհրաժեշտ է տրամադրել գոնե մեկ ստացողի e-mail հասցե.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP -ի սխալ: չի հաջողվել ուղարկել հետևյալ ստացողների հասցեներին: '; +$PHPMAILER_LANG['signing'] = 'Ստորագրման սխալ: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP -ի connect() ֆունկցիան չի հաջողվել'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP սերվերի սխալ: '; +$PHPMAILER_LANG['variable_set'] = 'Չի հաջողվում ստեղծել կամ վերափոխել փոփոխականը: '; +$PHPMAILER_LANG['extension_missing'] = 'Հավելվածը բացակայում է: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ar.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ar.php new file mode 100755 index 0000000..865d0b7 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ar.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'خطأ SMTP : لا يمكن تأكيد الهوية.'; +$PHPMAILER_LANG['connect_host'] = 'خطأ SMTP: لا يمكن الاتصال بالخادم SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'خطأ SMTP: لم يتم قبول المعلومات .'; +$PHPMAILER_LANG['empty_message'] = 'نص الرسالة فارغ'; +$PHPMAILER_LANG['encoding'] = 'ترميز غير معروف: '; +$PHPMAILER_LANG['execute'] = 'لا يمكن تنفيذ : '; +$PHPMAILER_LANG['file_access'] = 'لا يمكن الوصول للملف: '; +$PHPMAILER_LANG['file_open'] = 'خطأ في الملف: لا يمكن فتحه: '; +$PHPMAILER_LANG['from_failed'] = 'خطأ على مستوى عنوان المرسل : '; +$PHPMAILER_LANG['instantiate'] = 'لا يمكن توفير خدمة البريد.'; +$PHPMAILER_LANG['invalid_address'] = 'الإرسال غير ممكن لأن عنوان البريد الإلكتروني غير صالح: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' برنامج الإرسال غير مدعوم.'; +$PHPMAILER_LANG['provide_address'] = 'يجب توفير عنوان البريد الإلكتروني لمستلم واحد على الأقل.'; +$PHPMAILER_LANG['recipients_failed'] = 'خطأ SMTP: الأخطاء التالية ' . + 'فشل في الارسال لكل من : '; +$PHPMAILER_LANG['signing'] = 'خطأ في التوقيع: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() غير ممكن.'; +$PHPMAILER_LANG['smtp_error'] = 'خطأ على مستوى الخادم SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'لا يمكن تعيين أو إعادة تعيين متغير: '; +$PHPMAILER_LANG['extension_missing'] = 'الإضافة غير موجودة: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-az.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-az.php new file mode 100755 index 0000000..3749d83 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-az.php @@ -0,0 +1,26 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP Greška: Neuspjela prijava.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Greška: Nije moguće spojiti se sa SMTP serverom.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Greška: Podatci nisu prihvaćeni.'; +$PHPMAILER_LANG['empty_message'] = 'Sadržaj poruke je prazan.'; +$PHPMAILER_LANG['encoding'] = 'Nepoznata kriptografija: '; +$PHPMAILER_LANG['execute'] = 'Nije moguće izvršiti naredbu: '; +$PHPMAILER_LANG['file_access'] = 'Nije moguće pristupiti datoteci: '; +$PHPMAILER_LANG['file_open'] = 'Nije moguće otvoriti datoteku: '; +$PHPMAILER_LANG['from_failed'] = 'SMTP Greška: Slanje sa navedenih e-mail adresa nije uspjelo: '; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Greška: Slanje na navedene e-mail adrese nije uspjelo: '; +$PHPMAILER_LANG['instantiate'] = 'Ne mogu pokrenuti mail funkcionalnost.'; +$PHPMAILER_LANG['invalid_address'] = 'E-mail nije poslan. Neispravna e-mail adresa: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer nije podržan.'; +$PHPMAILER_LANG['provide_address'] = 'Definišite barem jednu adresu primaoca.'; +$PHPMAILER_LANG['signing'] = 'Greška prilikom prijave: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Spajanje na SMTP server nije uspjelo.'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP greška: '; +$PHPMAILER_LANG['variable_set'] = 'Nije moguće postaviti varijablu ili je vratiti nazad: '; +$PHPMAILER_LANG['extension_missing'] = 'Nedostaje ekstenzija: '; \ No newline at end of file diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-be.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-be.php new file mode 100755 index 0000000..e2f98f0 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-be.php @@ -0,0 +1,26 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'Памылка SMTP: памылка ідэнтыфікацыі.'; +$PHPMAILER_LANG['connect_host'] = 'Памылка SMTP: нельга ўстанавіць сувязь з SMTP-серверам.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Памылка SMTP: звесткі непрынятыя.'; +$PHPMAILER_LANG['empty_message'] = 'Пустое паведамленне.'; +$PHPMAILER_LANG['encoding'] = 'Невядомая кадыроўка тэксту: '; +$PHPMAILER_LANG['execute'] = 'Нельга выканаць каманду: '; +$PHPMAILER_LANG['file_access'] = 'Няма доступу да файла: '; +$PHPMAILER_LANG['file_open'] = 'Нельга адкрыць файл: '; +$PHPMAILER_LANG['from_failed'] = 'Няправільны адрас адпраўніка: '; +$PHPMAILER_LANG['instantiate'] = 'Нельга прымяніць функцыю mail().'; +$PHPMAILER_LANG['invalid_address'] = 'Нельга даслаць паведамленне, няправільны email атрымальніка: '; +$PHPMAILER_LANG['provide_address'] = 'Запоўніце, калі ласка, правільны email атрымальніка.'; +$PHPMAILER_LANG['mailer_not_supported'] = ' - паштовы сервер не падтрымліваецца.'; +$PHPMAILER_LANG['recipients_failed'] = 'Памылка SMTP: няправільныя атрымальнікі: '; +$PHPMAILER_LANG['signing'] = 'Памылка подпісу паведамлення: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Памылка сувязі з SMTP-серверам.'; +$PHPMAILER_LANG['smtp_error'] = 'Памылка SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Нельга ўстанавіць або перамяніць значэнне пераменнай: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-bg.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-bg.php new file mode 100755 index 0000000..b22941f --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-bg.php @@ -0,0 +1,26 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP грешка: Не може да се удостовери пред сървъра.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP грешка: Не може да се свърже с SMTP хоста.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP грешка: данните не са приети.'; +$PHPMAILER_LANG['empty_message'] = 'Съдържанието на съобщението е празно'; +$PHPMAILER_LANG['encoding'] = 'Неизвестно кодиране: '; +$PHPMAILER_LANG['execute'] = 'Не може да се изпълни: '; +$PHPMAILER_LANG['file_access'] = 'Няма достъп до файл: '; +$PHPMAILER_LANG['file_open'] = 'Файлова грешка: Не може да се отвори файл: '; +$PHPMAILER_LANG['from_failed'] = 'Следните адреси за подател са невалидни: '; +$PHPMAILER_LANG['instantiate'] = 'Не може да се инстанцира функцията mail.'; +$PHPMAILER_LANG['invalid_address'] = 'Невалиден адрес: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' - пощенски сървър не се поддържа.'; +$PHPMAILER_LANG['provide_address'] = 'Трябва да предоставите поне един email адрес за получател.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP грешка: Следните адреси за Получател са невалидни: '; +$PHPMAILER_LANG['signing'] = 'Грешка при подписване: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP провален connect().'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP сървърна грешка: '; +$PHPMAILER_LANG['variable_set'] = 'Не може да се установи или възстанови променлива: '; +$PHPMAILER_LANG['extension_missing'] = 'Липсва разширение: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ca.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ca.php new file mode 100755 index 0000000..4117596 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ca.php @@ -0,0 +1,26 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'Error SMTP: No s’ha pogut autenticar.'; +$PHPMAILER_LANG['connect_host'] = 'Error SMTP: No es pot connectar al servidor SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Error SMTP: Dades no acceptades.'; +$PHPMAILER_LANG['empty_message'] = 'El cos del missatge està buit.'; +$PHPMAILER_LANG['encoding'] = 'Codificació desconeguda: '; +$PHPMAILER_LANG['execute'] = 'No es pot executar: '; +$PHPMAILER_LANG['file_access'] = 'No es pot accedir a l’arxiu: '; +$PHPMAILER_LANG['file_open'] = 'Error d’Arxiu: No es pot obrir l’arxiu: '; +$PHPMAILER_LANG['from_failed'] = 'La(s) següent(s) adreces de remitent han fallat: '; +$PHPMAILER_LANG['instantiate'] = 'No s’ha pogut crear una instància de la funció Mail.'; +$PHPMAILER_LANG['invalid_address'] = 'Adreça d’email invalida: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer no està suportat'; +$PHPMAILER_LANG['provide_address'] = 'S’ha de proveir almenys una adreça d’email com a destinatari.'; +$PHPMAILER_LANG['recipients_failed'] = 'Error SMTP: Els següents destinataris han fallat: '; +$PHPMAILER_LANG['signing'] = 'Error al signar: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Ha fallat el SMTP Connect().'; +$PHPMAILER_LANG['smtp_error'] = 'Error del servidor SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'No s’ha pogut establir o restablir la variable: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ch.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ch.php new file mode 100755 index 0000000..4fda6b8 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ch.php @@ -0,0 +1,26 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP 错误:身份验证失败。'; +$PHPMAILER_LANG['connect_host'] = 'SMTP 错误: 不能连接SMTP主机。'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP 错误: 数据不可接受。'; +//$PHPMAILER_LANG['empty_message'] = 'Message body empty'; +$PHPMAILER_LANG['encoding'] = '未知编码:'; +$PHPMAILER_LANG['execute'] = '不能执行: '; +$PHPMAILER_LANG['file_access'] = '不能访问文件:'; +$PHPMAILER_LANG['file_open'] = '文件错误:不能打开文件:'; +$PHPMAILER_LANG['from_failed'] = '下面的发送地址邮件发送失败了: '; +$PHPMAILER_LANG['instantiate'] = '不能实现mail方法。'; +//$PHPMAILER_LANG['invalid_address'] = 'Invalid address: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' 您所选择的发送邮件的方法并不支持。'; +$PHPMAILER_LANG['provide_address'] = '您必须提供至少一个 收信人的email地址。'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP 错误: 下面的 收件人失败了: '; +//$PHPMAILER_LANG['signing'] = 'Signing Error: '; +//$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() failed.'; +//$PHPMAILER_LANG['smtp_error'] = 'SMTP server error: '; +//$PHPMAILER_LANG['variable_set'] = 'Cannot set or reset variable: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-cs.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-cs.php new file mode 100755 index 0000000..1160cf0 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-cs.php @@ -0,0 +1,25 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP fejl: Kunne ikke logge på.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP fejl: Kunne ikke tilslutte SMTP serveren.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP fejl: Data kunne ikke accepteres.'; +//$PHPMAILER_LANG['empty_message'] = 'Message body empty'; +$PHPMAILER_LANG['encoding'] = 'Ukendt encode-format: '; +$PHPMAILER_LANG['execute'] = 'Kunne ikke køre: '; +$PHPMAILER_LANG['file_access'] = 'Ingen adgang til fil: '; +$PHPMAILER_LANG['file_open'] = 'Fil fejl: Kunne ikke åbne filen: '; +$PHPMAILER_LANG['from_failed'] = 'Følgende afsenderadresse er forkert: '; +$PHPMAILER_LANG['instantiate'] = 'Kunne ikke initialisere email funktionen.'; +//$PHPMAILER_LANG['invalid_address'] = 'Invalid address: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer understøttes ikke.'; +$PHPMAILER_LANG['provide_address'] = 'Du skal indtaste mindst en modtagers emailadresse.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP fejl: Følgende modtagere er forkerte: '; +//$PHPMAILER_LANG['signing'] = 'Signing Error: '; +//$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() failed.'; +//$PHPMAILER_LANG['smtp_error'] = 'SMTP server error: '; +//$PHPMAILER_LANG['variable_set'] = 'Cannot set or reset variable: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-de.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-de.php new file mode 100755 index 0000000..aa987a9 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-de.php @@ -0,0 +1,25 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'Error SMTP: Imposible autentificar.'; +$PHPMAILER_LANG['connect_host'] = 'Error SMTP: Imposible conectar al servidor SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Error SMTP: Datos no aceptados.'; +$PHPMAILER_LANG['empty_message'] = 'El cuerpo del mensaje está vacío.'; +$PHPMAILER_LANG['encoding'] = 'Codificación desconocida: '; +$PHPMAILER_LANG['execute'] = 'Imposible ejecutar: '; +$PHPMAILER_LANG['file_access'] = 'Imposible acceder al archivo: '; +$PHPMAILER_LANG['file_open'] = 'Error de Archivo: Imposible abrir el archivo: '; +$PHPMAILER_LANG['from_failed'] = 'La(s) siguiente(s) direcciones de remitente fallaron: '; +$PHPMAILER_LANG['instantiate'] = 'Imposible crear una instancia de la función Mail.'; +$PHPMAILER_LANG['invalid_address'] = 'Imposible enviar: dirección de email inválido: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer no está soportado.'; +$PHPMAILER_LANG['provide_address'] = 'Debe proporcionar al menos una dirección de email de destino.'; +$PHPMAILER_LANG['recipients_failed'] = 'Error SMTP: Los siguientes destinos fallaron: '; +$PHPMAILER_LANG['signing'] = 'Error al firmar: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() falló.'; +$PHPMAILER_LANG['smtp_error'] = 'Error del servidor SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'No se pudo configurar la variable: '; +$PHPMAILER_LANG['extension_missing'] = 'Extensión faltante: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-et.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-et.php new file mode 100755 index 0000000..7e06da1 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-et.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP Viga: Autoriseerimise viga.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Viga: Ei õnnestunud luua ühendust SMTP serveriga.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Viga: Vigased andmed.'; +$PHPMAILER_LANG['empty_message'] = 'Tühi kirja sisu'; +$PHPMAILER_LANG["encoding"] = 'Tundmatu kodeering: '; +$PHPMAILER_LANG['execute'] = 'Tegevus ebaõnnestus: '; +$PHPMAILER_LANG['file_access'] = 'Pole piisavalt õiguseid järgneva faili avamiseks: '; +$PHPMAILER_LANG['file_open'] = 'Faili Viga: Faili avamine ebaõnnestus: '; +$PHPMAILER_LANG['from_failed'] = 'Järgnev saatja e-posti aadress on vigane: '; +$PHPMAILER_LANG['instantiate'] = 'mail funktiooni käivitamine ebaõnnestus.'; +$PHPMAILER_LANG['invalid_address'] = 'Saatmine peatatud, e-posti address vigane: '; +$PHPMAILER_LANG['provide_address'] = 'Te peate määrama vähemalt ühe saaja e-posti aadressi.'; +$PHPMAILER_LANG['mailer_not_supported'] = ' maileri tugi puudub.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Viga: Järgnevate saajate e-posti aadressid on vigased: '; +$PHPMAILER_LANG["signing"] = 'Viga allkirjastamisel: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() ebaõnnestus.'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP serveri viga: '; +$PHPMAILER_LANG['variable_set'] = 'Ei õnnestunud määrata või lähtestada muutujat: '; +$PHPMAILER_LANG['extension_missing'] = 'Nõutud laiendus on puudu: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-fa.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-fa.php new file mode 100755 index 0000000..ad0745c --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-fa.php @@ -0,0 +1,27 @@ + + * @author Mohammad Hossein Mojtahedi + */ + +$PHPMAILER_LANG['authenticate'] = 'خطای SMTP: احراز هویت با شکست مواجه شد.'; +$PHPMAILER_LANG['connect_host'] = 'خطای SMTP: اتصال به سرور SMTP برقرار نشد.'; +$PHPMAILER_LANG['data_not_accepted'] = 'خطای SMTP: داده‌ها نا‌درست هستند.'; +$PHPMAILER_LANG['empty_message'] = 'بخش متن پیام خالی است.'; +$PHPMAILER_LANG['encoding'] = 'کد‌گذاری نا‌شناخته: '; +$PHPMAILER_LANG['execute'] = 'امکان اجرا وجود ندارد: '; +$PHPMAILER_LANG['file_access'] = 'امکان دسترسی به فایل وجود ندارد: '; +$PHPMAILER_LANG['file_open'] = 'خطای File: امکان بازکردن فایل وجود ندارد: '; +$PHPMAILER_LANG['from_failed'] = 'آدرس فرستنده اشتباه است: '; +$PHPMAILER_LANG['instantiate'] = 'امکان معرفی تابع ایمیل وجود ندارد.'; +$PHPMAILER_LANG['invalid_address'] = 'آدرس ایمیل معتبر نیست: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer پشتیبانی نمی‌شود.'; +$PHPMAILER_LANG['provide_address'] = 'باید حداقل یک آدرس گیرنده وارد کنید.'; +$PHPMAILER_LANG['recipients_failed'] = 'خطای SMTP: ارسال به آدرس گیرنده با خطا مواجه شد: '; +$PHPMAILER_LANG['signing'] = 'خطا در امضا: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'خطا در اتصال به SMTP.'; +$PHPMAILER_LANG['smtp_error'] = 'خطا در SMTP Server: '; +$PHPMAILER_LANG['variable_set'] = 'امکان ارسال یا ارسال مجدد متغیر‌ها وجود ندارد: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-fi.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-fi.php new file mode 100755 index 0000000..ec4e752 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-fi.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP feilur: Kundi ikki góðkenna.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP feilur: Kundi ikki knýta samband við SMTP vert.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP feilur: Data ikki góðkent.'; +//$PHPMAILER_LANG['empty_message'] = 'Message body empty'; +$PHPMAILER_LANG['encoding'] = 'Ókend encoding: '; +$PHPMAILER_LANG['execute'] = 'Kundi ikki útføra: '; +$PHPMAILER_LANG['file_access'] = 'Kundi ikki tilganga fílu: '; +$PHPMAILER_LANG['file_open'] = 'Fílu feilur: Kundi ikki opna fílu: '; +$PHPMAILER_LANG['from_failed'] = 'fylgjandi Frá/From adressa miseydnaðist: '; +$PHPMAILER_LANG['instantiate'] = 'Kuni ikki instantiera mail funktión.'; +//$PHPMAILER_LANG['invalid_address'] = 'Invalid address: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' er ikki supporterað.'; +$PHPMAILER_LANG['provide_address'] = 'Tú skal uppgeva minst móttakara-emailadressu(r).'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Feilur: Fylgjandi móttakarar miseydnaðust: '; +//$PHPMAILER_LANG['signing'] = 'Signing Error: '; +//$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() failed.'; +//$PHPMAILER_LANG['smtp_error'] = 'SMTP server error: '; +//$PHPMAILER_LANG['variable_set'] = 'Cannot set or reset variable: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-fr.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-fr.php new file mode 100755 index 0000000..af68c92 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-fr.php @@ -0,0 +1,29 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'Erro SMTP: Non puido ser autentificado.'; +$PHPMAILER_LANG['connect_host'] = 'Erro SMTP: Non puido conectar co servidor SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Erro SMTP: Datos non aceptados.'; +$PHPMAILER_LANG['empty_message'] = 'Corpo da mensaxe vacía'; +$PHPMAILER_LANG['encoding'] = 'Codificación descoñecida: '; +$PHPMAILER_LANG['execute'] = 'Non puido ser executado: '; +$PHPMAILER_LANG['file_access'] = 'Nob puido acceder ó arquivo: '; +$PHPMAILER_LANG['file_open'] = 'Erro de Arquivo: No puido abrir o arquivo: '; +$PHPMAILER_LANG['from_failed'] = 'A(s) seguinte(s) dirección(s) de remitente(s) deron erro: '; +$PHPMAILER_LANG['instantiate'] = 'Non puido crear unha instancia da función Mail.'; +$PHPMAILER_LANG['invalid_address'] = 'Non puido envia-lo correo: dirección de email inválida: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer non está soportado.'; +$PHPMAILER_LANG['provide_address'] = 'Debe engadir polo menos unha dirección de email coma destino.'; +$PHPMAILER_LANG['recipients_failed'] = 'Erro SMTP: Os seguintes destinos fallaron: '; +$PHPMAILER_LANG['signing'] = 'Erro ó firmar: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() fallou.'; +$PHPMAILER_LANG['smtp_error'] = 'Erro do servidor SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Non puidemos axustar ou reaxustar a variábel: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-he.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-he.php new file mode 100755 index 0000000..70eb717 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-he.php @@ -0,0 +1,26 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'שגיאת SMTP: פעולת האימות נכשלה.'; +$PHPMAILER_LANG['connect_host'] = 'שגיאת SMTP: לא הצלחתי להתחבר לשרת SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'שגיאת SMTP: מידע לא התקבל.'; +$PHPMAILER_LANG['empty_message'] = 'גוף ההודעה ריק'; +$PHPMAILER_LANG['invalid_address'] = 'כתובת שגויה: '; +$PHPMAILER_LANG['encoding'] = 'קידוד לא מוכר: '; +$PHPMAILER_LANG['execute'] = 'לא הצלחתי להפעיל את: '; +$PHPMAILER_LANG['file_access'] = 'לא ניתן לגשת לקובץ: '; +$PHPMAILER_LANG['file_open'] = 'שגיאת קובץ: לא ניתן לגשת לקובץ: '; +$PHPMAILER_LANG['from_failed'] = 'כתובות הנמענים הבאות נכשלו: '; +$PHPMAILER_LANG['instantiate'] = 'לא הצלחתי להפעיל את פונקציית המייל.'; +$PHPMAILER_LANG['mailer_not_supported'] = ' אינה נתמכת.'; +$PHPMAILER_LANG['provide_address'] = 'חובה לספק לפחות כתובת אחת של מקבל המייל.'; +$PHPMAILER_LANG['recipients_failed'] = 'שגיאת SMTP: הנמענים הבאים נכשלו: '; +$PHPMAILER_LANG['signing'] = 'שגיאת חתימה: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() failed.'; +$PHPMAILER_LANG['smtp_error'] = 'שגיאת שרת SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'לא ניתן לקבוע או לשנות את המשתנה: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-hi.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-hi.php new file mode 100755 index 0000000..607a5ee --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-hi.php @@ -0,0 +1,26 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP त्रुटि: प्रामाणिकता की जांच नहीं हो सका। '; +$PHPMAILER_LANG['connect_host'] = 'SMTP त्रुटि: SMTP सर्वर से कनेक्ट नहीं हो सका। '; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP त्रुटि: डेटा स्वीकार नहीं किया जाता है। '; +$PHPMAILER_LANG['empty_message'] = 'संदेश खाली है। '; +$PHPMAILER_LANG['encoding'] = 'अज्ञात एन्कोडिंग प्रकार। '; +$PHPMAILER_LANG['execute'] = 'आदेश को निष्पादित करने में विफल। '; +$PHPMAILER_LANG['file_access'] = 'फ़ाइल उपलब्ध नहीं है। '; +$PHPMAILER_LANG['file_open'] = 'फ़ाइल त्रुटि: फाइल को खोला नहीं जा सका। '; +$PHPMAILER_LANG['from_failed'] = 'प्रेषक का पता गलत है। '; +$PHPMAILER_LANG['instantiate'] = 'मेल फ़ंक्शन कॉल नहीं कर सकता है।'; +$PHPMAILER_LANG['invalid_address'] = 'पता गलत है। '; +$PHPMAILER_LANG['mailer_not_supported'] = 'मेल सर्वर के साथ काम नहीं करता है। '; +$PHPMAILER_LANG['provide_address'] = 'आपको कम से कम एक प्राप्तकर्ता का ई-मेल पता प्रदान करना होगा।'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP त्रुटि: निम्न प्राप्तकर्ताओं को पते भेजने में विफल। '; +$PHPMAILER_LANG['signing'] = 'साइनअप त्रुटि:। '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP का connect () फ़ंक्शन विफल हुआ। '; +$PHPMAILER_LANG['smtp_error'] = 'SMTP सर्वर त्रुटि। '; +$PHPMAILER_LANG['variable_set'] = 'चर को बना या संशोधित नहीं किया जा सकता। '; +$PHPMAILER_LANG['extension_missing'] = 'एक्सटेन्षन गायब है: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-hr.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-hr.php new file mode 100755 index 0000000..3822920 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-hr.php @@ -0,0 +1,26 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP Greška: Neuspjela autentikacija.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Greška: Ne mogu se spojiti na SMTP poslužitelj.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Greška: Podatci nisu prihvaćeni.'; +$PHPMAILER_LANG['empty_message'] = 'Sadržaj poruke je prazan.'; +$PHPMAILER_LANG['encoding'] = 'Nepoznati encoding: '; +$PHPMAILER_LANG['execute'] = 'Nije moguće izvršiti naredbu: '; +$PHPMAILER_LANG['file_access'] = 'Nije moguće pristupiti datoteci: '; +$PHPMAILER_LANG['file_open'] = 'Nije moguće otvoriti datoteku: '; +$PHPMAILER_LANG['from_failed'] = 'SMTP Greška: Slanje s navedenih e-mail adresa nije uspjelo: '; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Greška: Slanje na navedenih e-mail adresa nije uspjelo: '; +$PHPMAILER_LANG['instantiate'] = 'Ne mogu pokrenuti mail funkcionalnost.'; +$PHPMAILER_LANG['invalid_address'] = 'E-mail nije poslan. Neispravna e-mail adresa: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer nije podržan.'; +$PHPMAILER_LANG['provide_address'] = 'Definirajte barem jednu adresu primatelja.'; +$PHPMAILER_LANG['signing'] = 'Greška prilikom prijave: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Spajanje na SMTP poslužitelj nije uspjelo.'; +$PHPMAILER_LANG['smtp_error'] = 'Greška SMTP poslužitelja: '; +$PHPMAILER_LANG['variable_set'] = 'Ne mogu postaviti varijablu niti ju vratiti nazad: '; +$PHPMAILER_LANG['extension_missing'] = 'Nedostaje proširenje: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-hu.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-hu.php new file mode 100755 index 0000000..196cddc --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-hu.php @@ -0,0 +1,26 @@ + + * @author @januridp + */ + +$PHPMAILER_LANG['authenticate'] = 'Kesalahan SMTP: Tidak dapat mengotentikasi.'; +$PHPMAILER_LANG['connect_host'] = 'Kesalahan SMTP: Tidak dapat terhubung ke host SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Kesalahan SMTP: Data tidak diterima.'; +$PHPMAILER_LANG['empty_message'] = 'Isi pesan kosong'; +$PHPMAILER_LANG['encoding'] = 'Pengkodean karakter tidak dikenali: '; +$PHPMAILER_LANG['execute'] = 'Tidak dapat menjalankan proses : '; +$PHPMAILER_LANG['file_access'] = 'Tidak dapat mengakses berkas : '; +$PHPMAILER_LANG['file_open'] = 'Kesalahan File: Berkas tidak dapat dibuka : '; +$PHPMAILER_LANG['from_failed'] = 'Alamat pengirim berikut mengakibatkan kesalahan : '; +$PHPMAILER_LANG['instantiate'] = 'Tidak dapat menginisialisasi fungsi surel'; +$PHPMAILER_LANG['invalid_address'] = 'Gagal terkirim, alamat surel tidak benar : '; +$PHPMAILER_LANG['provide_address'] = 'Harus disediakan minimal satu alamat tujuan'; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer tidak didukung'; +$PHPMAILER_LANG['recipients_failed'] = 'Kesalahan SMTP: Alamat tujuan berikut menghasilkan kesalahan : '; +$PHPMAILER_LANG['signing'] = 'Kesalahan dalam tanda tangan : '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() gagal.'; +$PHPMAILER_LANG['smtp_error'] = 'Kesalahan pada pelayan SMTP : '; +$PHPMAILER_LANG['variable_set'] = 'Tidak dapat mengatur atau mengatur ulang variable : '; +$PHPMAILER_LANG['extension_missing'] = 'Ekstensi hilang: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-it.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-it.php new file mode 100755 index 0000000..e67b6f7 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-it.php @@ -0,0 +1,27 @@ + + * @author Stefano Sabatini + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP Error: Impossibile autenticarsi.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Error: Impossibile connettersi all\'host SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Error: Dati non accettati dal server.'; +$PHPMAILER_LANG['empty_message'] = 'Il corpo del messaggio è vuoto'; +$PHPMAILER_LANG['encoding'] = 'Codifica dei caratteri sconosciuta: '; +$PHPMAILER_LANG['execute'] = 'Impossibile eseguire l\'operazione: '; +$PHPMAILER_LANG['file_access'] = 'Impossibile accedere al file: '; +$PHPMAILER_LANG['file_open'] = 'File Error: Impossibile aprire il file: '; +$PHPMAILER_LANG['from_failed'] = 'I seguenti indirizzi mittenti hanno generato errore: '; +$PHPMAILER_LANG['instantiate'] = 'Impossibile istanziare la funzione mail'; +$PHPMAILER_LANG['invalid_address'] = 'Impossibile inviare, l\'indirizzo email non è valido: '; +$PHPMAILER_LANG['provide_address'] = 'Deve essere fornito almeno un indirizzo ricevente'; +$PHPMAILER_LANG['mailer_not_supported'] = 'Mailer non supportato'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Error: I seguenti indirizzi destinatari hanno generato un errore: '; +$PHPMAILER_LANG['signing'] = 'Errore nella firma: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() fallita.'; +$PHPMAILER_LANG['smtp_error'] = 'Errore del server SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Impossibile impostare o resettare la variabile: '; +$PHPMAILER_LANG['extension_missing'] = 'Estensione mancante: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ja.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ja.php new file mode 100755 index 0000000..2d77872 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ja.php @@ -0,0 +1,27 @@ + + * @author Yoshi Sakai + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTPエラー: 認証できませんでした。'; +$PHPMAILER_LANG['connect_host'] = 'SMTPエラー: SMTPホストに接続できませんでした。'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTPエラー: データが受け付けられませんでした。'; +//$PHPMAILER_LANG['empty_message'] = 'Message body empty'; +$PHPMAILER_LANG['encoding'] = '不明なエンコーディング: '; +$PHPMAILER_LANG['execute'] = '実行できませんでした: '; +$PHPMAILER_LANG['file_access'] = 'ファイルにアクセスできません: '; +$PHPMAILER_LANG['file_open'] = 'ファイルエラー: ファイルを開けません: '; +$PHPMAILER_LANG['from_failed'] = 'Fromアドレスを登録する際にエラーが発生しました: '; +$PHPMAILER_LANG['instantiate'] = 'メール関数が正常に動作しませんでした。'; +//$PHPMAILER_LANG['invalid_address'] = 'Invalid address: '; +$PHPMAILER_LANG['provide_address'] = '少なくとも1つメールアドレスを 指定する必要があります。'; +$PHPMAILER_LANG['mailer_not_supported'] = ' メーラーがサポートされていません。'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTPエラー: 次の受信者アドレスに 間違いがあります: '; +//$PHPMAILER_LANG['signing'] = 'Signing Error: '; +//$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() failed.'; +//$PHPMAILER_LANG['smtp_error'] = 'SMTP server error: '; +//$PHPMAILER_LANG['variable_set'] = 'Cannot set or reset variable: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ka.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ka.php new file mode 100755 index 0000000..dd1af8a --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ka.php @@ -0,0 +1,26 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP შეცდომა: ავტორიზაცია შეუძლებელია.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP შეცდომა: SMTP სერვერთან დაკავშირება შეუძლებელია.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP შეცდომა: მონაცემები არ იქნა მიღებული.'; +$PHPMAILER_LANG['encoding'] = 'კოდირების უცნობი ტიპი: '; +$PHPMAILER_LANG['execute'] = 'შეუძლებელია შემდეგი ბრძანების შესრულება: '; +$PHPMAILER_LANG['file_access'] = 'შეუძლებელია წვდომა ფაილთან: '; +$PHPMAILER_LANG['file_open'] = 'ფაილური სისტემის შეცდომა: არ იხსნება ფაილი: '; +$PHPMAILER_LANG['from_failed'] = 'გამგზავნის არასწორი მისამართი: '; +$PHPMAILER_LANG['instantiate'] = 'mail ფუნქციის გაშვება ვერ ხერხდება.'; +$PHPMAILER_LANG['provide_address'] = 'გთხოვთ მიუთითოთ ერთი ადრესატის e-mail მისამართი მაინც.'; +$PHPMAILER_LANG['mailer_not_supported'] = ' - საფოსტო სერვერის მხარდაჭერა არ არის.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP შეცდომა: შემდეგ მისამართებზე გაგზავნა ვერ მოხერხდა: '; +$PHPMAILER_LANG['empty_message'] = 'შეტყობინება ცარიელია'; +$PHPMAILER_LANG['invalid_address'] = 'არ გაიგზავნა, e-mail მისამართის არასწორი ფორმატი: '; +$PHPMAILER_LANG['signing'] = 'ხელმოწერის შეცდომა: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'შეცდომა SMTP სერვერთან დაკავშირებისას'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP სერვერის შეცდომა: '; +$PHPMAILER_LANG['variable_set'] = 'შეუძლებელია შემდეგი ცვლადის შექმნა ან შეცვლა: '; +$PHPMAILER_LANG['extension_missing'] = 'ბიბლიოთეკა არ არსებობს: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ko.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ko.php new file mode 100755 index 0000000..9599fa6 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ko.php @@ -0,0 +1,26 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP 오류: 인증할 수 없습니다.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP 오류: SMTP 호스트에 접속할 수 없습니다.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP 오류: 데이터가 받아들여지지 않았습니다.'; +$PHPMAILER_LANG['empty_message'] = '메세지 내용이 없습니다'; +$PHPMAILER_LANG['encoding'] = '알 수 없는 인코딩: '; +$PHPMAILER_LANG['execute'] = '실행 불가: '; +$PHPMAILER_LANG['file_access'] = '파일 접근 불가: '; +$PHPMAILER_LANG['file_open'] = '파일 오류: 파일을 열 수 없습니다: '; +$PHPMAILER_LANG['from_failed'] = '다음 From 주소에서 오류가 발생했습니다: '; +$PHPMAILER_LANG['instantiate'] = 'mail 함수를 인스턴스화할 수 없습니다'; +$PHPMAILER_LANG['invalid_address'] = '잘못된 주소: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' 메일러는 지원되지 않습니다.'; +$PHPMAILER_LANG['provide_address'] = '적어도 한 개 이상의 수신자 메일 주소를 제공해야 합니다.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP 오류: 다음 수신자에서 오류가 발생했습니다: '; +$PHPMAILER_LANG['signing'] = '서명 오류: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP 연결을 실패하였습니다.'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP 서버 오류: '; +$PHPMAILER_LANG['variable_set'] = '변수 설정 및 초기화 불가: '; +$PHPMAILER_LANG['extension_missing'] = '확장자 없음: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-lt.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-lt.php new file mode 100755 index 0000000..1253a4f --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-lt.php @@ -0,0 +1,26 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP klaida: autentifikacija nepavyko.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP klaida: nepavyksta prisijungti prie SMTP stoties.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP klaida: duomenys nepriimti.'; +$PHPMAILER_LANG['empty_message'] = 'Laiško turinys tuščias'; +$PHPMAILER_LANG['encoding'] = 'Neatpažinta koduotė: '; +$PHPMAILER_LANG['execute'] = 'Nepavyko įvykdyti komandos: '; +$PHPMAILER_LANG['file_access'] = 'Byla nepasiekiama: '; +$PHPMAILER_LANG['file_open'] = 'Bylos klaida: Nepavyksta atidaryti: '; +$PHPMAILER_LANG['from_failed'] = 'Neteisingas siuntėjo adresas: '; +$PHPMAILER_LANG['instantiate'] = 'Nepavyko paleisti mail funkcijos.'; +$PHPMAILER_LANG['invalid_address'] = 'Neteisingas adresas: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' pašto stotis nepalaikoma.'; +$PHPMAILER_LANG['provide_address'] = 'Nurodykite bent vieną gavėjo adresą.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP klaida: nepavyko išsiųsti šiems gavėjams: '; +$PHPMAILER_LANG['signing'] = 'Prisijungimo klaida: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP susijungimo klaida'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP stoties klaida: '; +$PHPMAILER_LANG['variable_set'] = 'Nepavyko priskirti reikšmės kintamajam: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-lv.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-lv.php new file mode 100755 index 0000000..39bf9a1 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-lv.php @@ -0,0 +1,26 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP kļūda: Autorizācija neizdevās.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Kļūda: Nevar izveidot savienojumu ar SMTP serveri.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Kļūda: Nepieņem informāciju.'; +$PHPMAILER_LANG['empty_message'] = 'Ziņojuma teksts ir tukšs'; +$PHPMAILER_LANG['encoding'] = 'Neatpazīts kodējums: '; +$PHPMAILER_LANG['execute'] = 'Neizdevās izpildīt komandu: '; +$PHPMAILER_LANG['file_access'] = 'Fails nav pieejams: '; +$PHPMAILER_LANG['file_open'] = 'Faila kļūda: Nevar atvērt failu: '; +$PHPMAILER_LANG['from_failed'] = 'Nepareiza sūtītāja adrese: '; +$PHPMAILER_LANG['instantiate'] = 'Nevar palaist sūtīšanas funkciju.'; +$PHPMAILER_LANG['invalid_address'] = 'Nepareiza adrese: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' sūtītājs netiek atbalstīts.'; +$PHPMAILER_LANG['provide_address'] = 'Lūdzu, norādiet vismaz vienu adresātu.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP kļūda: neizdevās nosūtīt šādiem saņēmējiem: '; +$PHPMAILER_LANG['signing'] = 'Autorizācijas kļūda: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP savienojuma kļūda'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP servera kļūda: '; +$PHPMAILER_LANG['variable_set'] = 'Nevar piešķirt mainīgā vērtību: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-mg.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-mg.php new file mode 100755 index 0000000..f4c7563 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-mg.php @@ -0,0 +1,25 @@ + + */ +$PHPMAILER_LANG['authenticate'] = 'Hadisoana SMTP: Tsy nahomby ny fanamarinana.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Error: Tsy afaka mampifandray amin\'ny mpampiantrano SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP diso: tsy voarakitra ny angona.'; +$PHPMAILER_LANG['empty_message'] = 'Tsy misy ny votoaty mailaka.'; +$PHPMAILER_LANG['encoding'] = 'Tsy fantatra encoding: '; +$PHPMAILER_LANG['execute'] = 'Tsy afaka manatanteraka ity baiko manaraka ity: '; +$PHPMAILER_LANG['file_access'] = 'Tsy nahomby ny fidirana amin\'ity rakitra ity: '; +$PHPMAILER_LANG['file_open'] = 'Hadisoana diso: Tsy afaka nanokatra ity file manaraka ity: '; +$PHPMAILER_LANG['from_failed'] = 'Ny adiresy iraka manaraka dia diso: '; +$PHPMAILER_LANG['instantiate'] = 'Tsy afaka nanomboka ny hetsika mail.'; +$PHPMAILER_LANG['invalid_address'] = 'Tsy mety ny adiresy: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer tsy manohana.'; +$PHPMAILER_LANG['provide_address'] = 'Alefaso azafady iray adiresy iray farafahakeliny.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Error: Tsy mety ireo mpanaraka ireto: '; +$PHPMAILER_LANG['signing'] = 'Error nandritra ny sonia:'; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Tsy nahomby ny fifandraisana tamin\'ny server SMTP.'; +$PHPMAILER_LANG['smtp_error'] = 'Fahadisoana tamin\'ny server SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Tsy azo atao ny mametraka na mamerina ny variable: '; +$PHPMAILER_LANG['extension_missing'] = 'Tsy hita ny ampahany: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ms.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ms.php new file mode 100755 index 0000000..f12a6ad --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ms.php @@ -0,0 +1,26 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'Ralat SMTP: Tidak dapat pengesahan.'; +$PHPMAILER_LANG['connect_host'] = 'Ralat SMTP: Tidak dapat menghubungi hos pelayan SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Ralat SMTP: Data tidak diterima oleh pelayan.'; +$PHPMAILER_LANG['empty_message'] = 'Tiada isi untuk mesej'; +$PHPMAILER_LANG['encoding'] = 'Pengekodan tidak diketahui: '; +$PHPMAILER_LANG['execute'] = 'Tidak dapat melaksanakan: '; +$PHPMAILER_LANG['file_access'] = 'Tidak dapat mengakses fail: '; +$PHPMAILER_LANG['file_open'] = 'Ralat Fail: Tidak dapat membuka fail: '; +$PHPMAILER_LANG['from_failed'] = 'Berikut merupakan ralat dari alamat e-mel: '; +$PHPMAILER_LANG['instantiate'] = 'Tidak dapat memberi contoh fungsi e-mel.'; +$PHPMAILER_LANG['invalid_address'] = 'Alamat emel tidak sah: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' jenis penghantar emel tidak disokong.'; +$PHPMAILER_LANG['provide_address'] = 'Anda perlu menyediakan sekurang-kurangnya satu alamat e-mel penerima.'; +$PHPMAILER_LANG['recipients_failed'] = 'Ralat SMTP: Penerima e-mel berikut telah gagal: '; +$PHPMAILER_LANG['signing'] = 'Ralat pada tanda tangan: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() telah gagal.'; +$PHPMAILER_LANG['smtp_error'] = 'Ralat pada pelayan SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Tidak boleh menetapkan atau menetapkan semula pembolehubah: '; +$PHPMAILER_LANG['extension_missing'] = 'Sambungan hilang: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-nb.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-nb.php new file mode 100755 index 0000000..97403e7 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-nb.php @@ -0,0 +1,25 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP-fout: authenticatie mislukt.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP-fout: kon niet verbinden met SMTP-host.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP-fout: data niet geaccepteerd.'; +$PHPMAILER_LANG['empty_message'] = 'Berichttekst is leeg'; +$PHPMAILER_LANG['encoding'] = 'Onbekende codering: '; +$PHPMAILER_LANG['execute'] = 'Kon niet uitvoeren: '; +$PHPMAILER_LANG['file_access'] = 'Kreeg geen toegang tot bestand: '; +$PHPMAILER_LANG['file_open'] = 'Bestandsfout: kon bestand niet openen: '; +$PHPMAILER_LANG['from_failed'] = 'Het volgende afzendersadres is mislukt: '; +$PHPMAILER_LANG['instantiate'] = 'Kon mailfunctie niet initialiseren.'; +$PHPMAILER_LANG['invalid_address'] = 'Ongeldig adres: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer wordt niet ondersteund.'; +$PHPMAILER_LANG['provide_address'] = 'Er moet minstens één ontvanger worden opgegeven.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP-fout: de volgende ontvangers zijn mislukt: '; +$PHPMAILER_LANG['signing'] = 'Signeerfout: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Verbinding mislukt.'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP-serverfout: '; +$PHPMAILER_LANG['variable_set'] = 'Kan de volgende variabele niet instellen of resetten: '; +$PHPMAILER_LANG['extension_missing'] = 'Extensie afwezig: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-pl.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-pl.php new file mode 100755 index 0000000..3da0dee --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-pl.php @@ -0,0 +1,26 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'Erro do SMTP: Não foi possível realizar a autenticação.'; +$PHPMAILER_LANG['connect_host'] = 'Erro do SMTP: Não foi possível realizar ligação com o servidor SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Erro do SMTP: Os dados foram rejeitados.'; +$PHPMAILER_LANG['empty_message'] = 'A mensagem no e-mail está vazia.'; +$PHPMAILER_LANG['encoding'] = 'Codificação desconhecida: '; +$PHPMAILER_LANG['execute'] = 'Não foi possível executar: '; +$PHPMAILER_LANG['file_access'] = 'Não foi possível aceder o ficheiro: '; +$PHPMAILER_LANG['file_open'] = 'Abertura do ficheiro: Não foi possível abrir o ficheiro: '; +$PHPMAILER_LANG['from_failed'] = 'Ocorreram falhas nos endereços dos seguintes remententes: '; +$PHPMAILER_LANG['instantiate'] = 'Não foi possível iniciar uma instância da função mail.'; +$PHPMAILER_LANG['invalid_address'] = 'Não foi enviado nenhum e-mail para o endereço de e-mail inválido: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer não é suportado.'; +$PHPMAILER_LANG['provide_address'] = 'Tem de fornecer pelo menos um endereço como destinatário do e-mail.'; +$PHPMAILER_LANG['recipients_failed'] = 'Erro do SMTP: O endereço do seguinte destinatário falhou: '; +$PHPMAILER_LANG['signing'] = 'Erro ao assinar: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() falhou.'; +$PHPMAILER_LANG['smtp_error'] = 'Erro de servidor SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Não foi possível definir ou redefinir a variável: '; +$PHPMAILER_LANG['extension_missing'] = 'Extensão em falta: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-pt_br.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-pt_br.php new file mode 100755 index 0000000..4ec10f7 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-pt_br.php @@ -0,0 +1,29 @@ + + * @author Lucas Guimarães + * @author Phelipe Alves + * @author Fabio Beneditto + */ + +$PHPMAILER_LANG['authenticate'] = 'Erro de SMTP: Não foi possível autenticar.'; +$PHPMAILER_LANG['connect_host'] = 'Erro de SMTP: Não foi possível conectar ao servidor SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Erro de SMTP: Dados rejeitados.'; +$PHPMAILER_LANG['empty_message'] = 'Mensagem vazia'; +$PHPMAILER_LANG['encoding'] = 'Codificação desconhecida: '; +$PHPMAILER_LANG['execute'] = 'Não foi possível executar: '; +$PHPMAILER_LANG['file_access'] = 'Não foi possível acessar o arquivo: '; +$PHPMAILER_LANG['file_open'] = 'Erro de Arquivo: Não foi possível abrir o arquivo: '; +$PHPMAILER_LANG['from_failed'] = 'Os seguintes remetentes falharam: '; +$PHPMAILER_LANG['instantiate'] = 'Não foi possível instanciar a função mail.'; +$PHPMAILER_LANG['invalid_address'] = 'Endereço de e-mail inválido: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer não é suportado.'; +$PHPMAILER_LANG['provide_address'] = 'Você deve informar pelo menos um destinatário.'; +$PHPMAILER_LANG['recipients_failed'] = 'Erro de SMTP: Os seguintes destinatários falharam: '; +$PHPMAILER_LANG['signing'] = 'Erro de Assinatura: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() falhou.'; +$PHPMAILER_LANG['smtp_error'] = 'Erro de servidor SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Não foi possível definir ou redefinir a variável: '; +$PHPMAILER_LANG['extension_missing'] = 'Extensão ausente: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ro.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ro.php new file mode 100755 index 0000000..fa100ea --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ro.php @@ -0,0 +1,26 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'Eroare SMTP: Autentificarea a eșuat.'; +$PHPMAILER_LANG['connect_host'] = 'Eroare SMTP: Conectarea la serverul SMTP a eșuat.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Eroare SMTP: Datele nu au fost acceptate.'; +$PHPMAILER_LANG['empty_message'] = 'Mesajul este gol.'; +$PHPMAILER_LANG['encoding'] = 'Encodare necunoscută: '; +$PHPMAILER_LANG['execute'] = 'Nu se poate executa următoarea comandă: '; +$PHPMAILER_LANG['file_access'] = 'Nu se poate accesa următorul fișier: '; +$PHPMAILER_LANG['file_open'] = 'Eroare fișier: Nu se poate deschide următorul fișier: '; +$PHPMAILER_LANG['from_failed'] = 'Următoarele adrese From au dat eroare: '; +$PHPMAILER_LANG['instantiate'] = 'Funcția mail nu a putut fi inițializată.'; +$PHPMAILER_LANG['invalid_address'] = 'Adresa de email nu este validă: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer nu este suportat.'; +$PHPMAILER_LANG['provide_address'] = 'Trebuie să adăugați cel puțin o adresă de email.'; +$PHPMAILER_LANG['recipients_failed'] = 'Eroare SMTP: Următoarele adrese de email au eșuat: '; +$PHPMAILER_LANG['signing'] = 'A aparut o problemă la semnarea emailului. '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Conectarea la serverul SMTP a eșuat.'; +$PHPMAILER_LANG['smtp_error'] = 'Eroare server SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Nu se poate seta/reseta variabila. '; +$PHPMAILER_LANG['extension_missing'] = 'Lipsește extensia: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ru.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ru.php new file mode 100755 index 0000000..4066f6b --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-ru.php @@ -0,0 +1,27 @@ + + * @author Foster Snowhill + */ + +$PHPMAILER_LANG['authenticate'] = 'Ошибка SMTP: ошибка авторизации.'; +$PHPMAILER_LANG['connect_host'] = 'Ошибка SMTP: не удается подключиться к серверу SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Ошибка SMTP: данные не приняты.'; +$PHPMAILER_LANG['encoding'] = 'Неизвестный вид кодировки: '; +$PHPMAILER_LANG['execute'] = 'Невозможно выполнить команду: '; +$PHPMAILER_LANG['file_access'] = 'Нет доступа к файлу: '; +$PHPMAILER_LANG['file_open'] = 'Файловая ошибка: не удается открыть файл: '; +$PHPMAILER_LANG['from_failed'] = 'Неверный адрес отправителя: '; +$PHPMAILER_LANG['instantiate'] = 'Невозможно запустить функцию mail.'; +$PHPMAILER_LANG['provide_address'] = 'Пожалуйста, введите хотя бы один адрес e-mail получателя.'; +$PHPMAILER_LANG['mailer_not_supported'] = ' — почтовый сервер не поддерживается.'; +$PHPMAILER_LANG['recipients_failed'] = 'Ошибка SMTP: отправка по следующим адресам получателей не удалась: '; +$PHPMAILER_LANG['empty_message'] = 'Пустое сообщение'; +$PHPMAILER_LANG['invalid_address'] = 'Не отослано, неправильный формат email адреса: '; +$PHPMAILER_LANG['signing'] = 'Ошибка подписи: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Ошибка соединения с SMTP-сервером'; +$PHPMAILER_LANG['smtp_error'] = 'Ошибка SMTP-сервера: '; +$PHPMAILER_LANG['variable_set'] = 'Невозможно установить или переустановить переменную: '; +$PHPMAILER_LANG['extension_missing'] = 'Расширение отсутствует: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-sk.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-sk.php new file mode 100755 index 0000000..69cfb0f --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-sk.php @@ -0,0 +1,27 @@ + + * @author Peter Orlický + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP Error: Chyba autentifikácie.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Error: Nebolo možné nadviazať spojenie so SMTP serverom.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Error: Dáta neboli prijaté'; +$PHPMAILER_LANG['empty_message'] = 'Prázdne telo správy.'; +$PHPMAILER_LANG['encoding'] = 'Neznáme kódovanie: '; +$PHPMAILER_LANG['execute'] = 'Nedá sa vykonať: '; +$PHPMAILER_LANG['file_access'] = 'Súbor nebol nájdený: '; +$PHPMAILER_LANG['file_open'] = 'File Error: Súbor sa otvoriť pre čítanie: '; +$PHPMAILER_LANG['from_failed'] = 'Následujúca adresa From je nesprávna: '; +$PHPMAILER_LANG['instantiate'] = 'Nedá sa vytvoriť inštancia emailovej funkcie.'; +$PHPMAILER_LANG['invalid_address'] = 'Neodoslané, emailová adresa je nesprávna: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' emailový klient nieje podporovaný.'; +$PHPMAILER_LANG['provide_address'] = 'Musíte zadať aspoň jednu emailovú adresu príjemcu.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Error: Adresy príjemcov niesu správne '; +$PHPMAILER_LANG['signing'] = 'Chyba prihlasovania: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() zlyhalo.'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP chyba serveru: '; +$PHPMAILER_LANG['variable_set'] = 'Nemožno nastaviť alebo resetovať premennú: '; +$PHPMAILER_LANG['extension_missing'] = 'Chýba rozšírenie: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-sl.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-sl.php new file mode 100755 index 0000000..1e3cb7f --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-sl.php @@ -0,0 +1,27 @@ + + * @author Filip Š + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP napaka: Avtentikacija ni uspela.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP napaka: Vzpostavljanje povezave s SMTP gostiteljem ni uspelo.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP napaka: Strežnik zavrača podatke.'; +$PHPMAILER_LANG['empty_message'] = 'E-poštno sporočilo nima vsebine.'; +$PHPMAILER_LANG['encoding'] = 'Nepoznan tip kodiranja: '; +$PHPMAILER_LANG['execute'] = 'Operacija ni uspela: '; +$PHPMAILER_LANG['file_access'] = 'Nimam dostopa do datoteke: '; +$PHPMAILER_LANG['file_open'] = 'Ne morem odpreti datoteke: '; +$PHPMAILER_LANG['from_failed'] = 'Neveljaven e-naslov pošiljatelja: '; +$PHPMAILER_LANG['instantiate'] = 'Ne morem inicializirati mail funkcije.'; +$PHPMAILER_LANG['invalid_address'] = 'E-poštno sporočilo ni bilo poslano. E-naslov je neveljaven: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer ni podprt.'; +$PHPMAILER_LANG['provide_address'] = 'Prosim vnesite vsaj enega naslovnika.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP napaka: Sledeči naslovniki so neveljavni: '; +$PHPMAILER_LANG['signing'] = 'Napaka pri podpisovanju: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Ne morem vzpostaviti povezave s SMTP strežnikom.'; +$PHPMAILER_LANG['smtp_error'] = 'Napaka SMTP strežnika: '; +$PHPMAILER_LANG['variable_set'] = 'Ne morem nastaviti oz. ponastaviti spremenljivke: '; +$PHPMAILER_LANG['extension_missing'] = 'Manjkajoča razširitev: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-sr.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-sr.php new file mode 100755 index 0000000..34c1e18 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-sr.php @@ -0,0 +1,27 @@ + + * @author Miloš Milanović + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP грешка: аутентификација није успела.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP грешка: повезивање са SMTP сервером није успело.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP грешка: подаци нису прихваћени.'; +$PHPMAILER_LANG['empty_message'] = 'Садржај поруке је празан.'; +$PHPMAILER_LANG['encoding'] = 'Непознато кодирање: '; +$PHPMAILER_LANG['execute'] = 'Није могуће извршити наредбу: '; +$PHPMAILER_LANG['file_access'] = 'Није могуће приступити датотеци: '; +$PHPMAILER_LANG['file_open'] = 'Није могуће отворити датотеку: '; +$PHPMAILER_LANG['from_failed'] = 'SMTP грешка: слање са следећих адреса није успело: '; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP грешка: слање на следеће адресе није успело: '; +$PHPMAILER_LANG['instantiate'] = 'Није могуће покренути mail функцију.'; +$PHPMAILER_LANG['invalid_address'] = 'Порука није послата. Неисправна адреса: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' мејлер није подржан.'; +$PHPMAILER_LANG['provide_address'] = 'Дефинишите бар једну адресу примаоца.'; +$PHPMAILER_LANG['signing'] = 'Грешка приликом пријаве: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Повезивање са SMTP сервером није успело.'; +$PHPMAILER_LANG['smtp_error'] = 'Грешка SMTP сервера: '; +$PHPMAILER_LANG['variable_set'] = 'Није могуће задати нити ресетовати променљиву: '; +$PHPMAILER_LANG['extension_missing'] = 'Недостаје проширење: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-sv.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-sv.php new file mode 100755 index 0000000..4408e63 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-sv.php @@ -0,0 +1,26 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP fel: Kunde inte autentisera.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP fel: Kunde inte ansluta till SMTP-server.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP fel: Data accepterades inte.'; +//$PHPMAILER_LANG['empty_message'] = 'Message body empty'; +$PHPMAILER_LANG['encoding'] = 'Okänt encode-format: '; +$PHPMAILER_LANG['execute'] = 'Kunde inte köra: '; +$PHPMAILER_LANG['file_access'] = 'Ingen åtkomst till fil: '; +$PHPMAILER_LANG['file_open'] = 'Fil fel: Kunde inte öppna fil: '; +$PHPMAILER_LANG['from_failed'] = 'Följande avsändaradress är felaktig: '; +$PHPMAILER_LANG['instantiate'] = 'Kunde inte initiera e-postfunktion.'; +$PHPMAILER_LANG['invalid_address'] = 'Felaktig adress: '; +$PHPMAILER_LANG['provide_address'] = 'Du måste ange minst en mottagares e-postadress.'; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer stöds inte.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP fel: Följande mottagare är felaktig: '; +$PHPMAILER_LANG['signing'] = 'Signerings fel: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() misslyckades.'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP server fel: '; +$PHPMAILER_LANG['variable_set'] = 'Kunde inte definiera eller återställa variabel: '; +$PHPMAILER_LANG['extension_missing'] = 'Tillägg ej tillgängligt: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-tl.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-tl.php new file mode 100755 index 0000000..ed51d4c --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-tl.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP Error: Hindi mapatotohanan.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Error: Hindi makakonekta sa SMTP host.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Error: Ang datos ay hindi maaaring matatanggap.'; +$PHPMAILER_LANG['empty_message'] = 'Walang laman ang mensahe'; +$PHPMAILER_LANG['encoding'] = 'Hindi alam ang encoding: '; +$PHPMAILER_LANG['execute'] = 'Hindi maisasagawa: '; +$PHPMAILER_LANG['file_access'] = 'Hindi ma-access ang file: '; +$PHPMAILER_LANG['file_open'] = 'Hindi mabuksan ang file: '; +$PHPMAILER_LANG['from_failed'] = 'Ang sumusunod na address ay nabigo: '; +$PHPMAILER_LANG['instantiate'] = 'Hindi maaaring magbigay ng institusyon ang mail'; +$PHPMAILER_LANG['invalid_address'] = 'Hindi wasto ang address na naibigay: '; +$PHPMAILER_LANG['mailer_not_supported'] = 'Ang mailer ay hindi suportado'; +$PHPMAILER_LANG['provide_address'] = 'Kailangan mong magbigay ng kahit isang email address na tatanggap'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Error: Ang mga sumusunod na tatanggap ay nabigo: '; +$PHPMAILER_LANG['signing'] = 'Hindi ma-sign'; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Ang SMTP connect() ay nabigo'; +$PHPMAILER_LANG['smtp_error'] = 'Ang server ng SMTP ay nabigo'; +$PHPMAILER_LANG['variable_set'] = 'Hindi matatakda ang mga variables: '; +$PHPMAILER_LANG['extension_missing'] = 'Nawawala ang extension'; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-tr.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-tr.php new file mode 100755 index 0000000..cfe8eaa --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-tr.php @@ -0,0 +1,30 @@ + + * @fixed by Boris Yurchenko + */ + +$PHPMAILER_LANG['authenticate'] = 'Помилка SMTP: помилка авторизації.'; +$PHPMAILER_LANG['connect_host'] = 'Помилка SMTP: не вдається під\'єднатися до серверу SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Помилка SMTP: дані не прийняті.'; +$PHPMAILER_LANG['encoding'] = 'Невідомий тип кодування: '; +$PHPMAILER_LANG['execute'] = 'Неможливо виконати команду: '; +$PHPMAILER_LANG['file_access'] = 'Немає доступу до файлу: '; +$PHPMAILER_LANG['file_open'] = 'Помилка файлової системи: не вдається відкрити файл: '; +$PHPMAILER_LANG['from_failed'] = 'Невірна адреса відправника: '; +$PHPMAILER_LANG['instantiate'] = 'Неможливо запустити функцію mail.'; +$PHPMAILER_LANG['provide_address'] = 'Будь-ласка, введіть хоча б одну адресу e-mail отримувача.'; +$PHPMAILER_LANG['mailer_not_supported'] = ' - поштовий сервер не підтримується.'; +$PHPMAILER_LANG['recipients_failed'] = 'Помилка SMTP: відправлення наступним отримувачам не вдалося: '; +$PHPMAILER_LANG['empty_message'] = 'Пусте тіло повідомлення'; +$PHPMAILER_LANG['invalid_address'] = 'Не відправлено, невірний формат адреси e-mail: '; +$PHPMAILER_LANG['signing'] = 'Помилка підпису: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Помилка з\'єднання із SMTP-сервером'; +$PHPMAILER_LANG['smtp_error'] = 'Помилка SMTP-сервера: '; +$PHPMAILER_LANG['variable_set'] = 'Неможливо встановити або перевстановити змінну: '; +$PHPMAILER_LANG['extension_missing'] = 'Не знайдено розширення: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-vi.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-vi.php new file mode 100755 index 0000000..c60dade --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-vi.php @@ -0,0 +1,26 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'Lỗi SMTP: Không thể xác thực.'; +$PHPMAILER_LANG['connect_host'] = 'Lỗi SMTP: Không thể kết nối máy chủ SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Lỗi SMTP: Dữ liệu không được chấp nhận.'; +$PHPMAILER_LANG['empty_message'] = 'Không có nội dung'; +$PHPMAILER_LANG['encoding'] = 'Mã hóa không xác định: '; +$PHPMAILER_LANG['execute'] = 'Không thực hiện được: '; +$PHPMAILER_LANG['file_access'] = 'Không thể truy cập tệp tin '; +$PHPMAILER_LANG['file_open'] = 'Lỗi Tập tin: Không thể mở tệp tin: '; +$PHPMAILER_LANG['from_failed'] = 'Lỗi địa chỉ gửi đi: '; +$PHPMAILER_LANG['instantiate'] = 'Không dùng được các hàm gửi thư.'; +$PHPMAILER_LANG['invalid_address'] = 'Đại chỉ emai không đúng: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' trình gửi thư không được hỗ trợ.'; +$PHPMAILER_LANG['provide_address'] = 'Bạn phải cung cấp ít nhất một địa chỉ người nhận.'; +$PHPMAILER_LANG['recipients_failed'] = 'Lỗi SMTP: lỗi địa chỉ người nhận: '; +$PHPMAILER_LANG['signing'] = 'Lỗi đăng nhập: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Lỗi kết nối với SMTP'; +$PHPMAILER_LANG['smtp_error'] = 'Lỗi máy chủ smtp '; +$PHPMAILER_LANG['variable_set'] = 'Không thể thiết lập hoặc thiết lập lại biến: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-zh.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-zh.php new file mode 100755 index 0000000..3e9e358 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-zh.php @@ -0,0 +1,28 @@ + + * @author Peter Dave Hello <@PeterDaveHello/> + * @author Jason Chiang + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP 錯誤:登入失敗。'; +$PHPMAILER_LANG['connect_host'] = 'SMTP 錯誤:無法連線到 SMTP 主機。'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP 錯誤:無法接受的資料。'; +$PHPMAILER_LANG['empty_message'] = '郵件內容為空'; +$PHPMAILER_LANG['encoding'] = '未知編碼: '; +$PHPMAILER_LANG['execute'] = '無法執行:'; +$PHPMAILER_LANG['file_access'] = '無法存取檔案:'; +$PHPMAILER_LANG['file_open'] = '檔案錯誤:無法開啟檔案:'; +$PHPMAILER_LANG['from_failed'] = '發送地址錯誤:'; +$PHPMAILER_LANG['instantiate'] = '未知函數呼叫。'; +$PHPMAILER_LANG['invalid_address'] = '因為電子郵件地址無效,無法傳送: '; +$PHPMAILER_LANG['mailer_not_supported'] = '不支援的發信客戶端。'; +$PHPMAILER_LANG['provide_address'] = '必須提供至少一個收件人地址。'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP 錯誤:以下收件人地址錯誤:'; +$PHPMAILER_LANG['signing'] = '電子簽章錯誤: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP 連線失敗'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP 伺服器錯誤: '; +$PHPMAILER_LANG['variable_set'] = '無法設定或重設變數: '; +$PHPMAILER_LANG['extension_missing'] = '遺失模組 Extension: '; diff --git a/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-zh_cn.php b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-zh_cn.php new file mode 100755 index 0000000..3753780 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/language/phpmailer.lang-zh_cn.php @@ -0,0 +1,28 @@ + + * @author young + * @author Teddysun + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP 错误:登录失败。'; +$PHPMAILER_LANG['connect_host'] = 'SMTP 错误:无法连接到 SMTP 主机。'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP 错误:数据不被接受。'; +$PHPMAILER_LANG['empty_message'] = '邮件正文为空。'; +$PHPMAILER_LANG['encoding'] = '未知编码:'; +$PHPMAILER_LANG['execute'] = '无法执行:'; +$PHPMAILER_LANG['file_access'] = '无法访问文件:'; +$PHPMAILER_LANG['file_open'] = '文件错误:无法打开文件:'; +$PHPMAILER_LANG['from_failed'] = '发送地址错误:'; +$PHPMAILER_LANG['instantiate'] = '未知函数调用。'; +$PHPMAILER_LANG['invalid_address'] = '发送失败,电子邮箱地址是无效的:'; +$PHPMAILER_LANG['mailer_not_supported'] = '发信客户端不被支持。'; +$PHPMAILER_LANG['provide_address'] = '必须提供至少一个收件人地址。'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP 错误:收件人地址错误:'; +$PHPMAILER_LANG['signing'] = '登录失败:'; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP服务器连接失败。'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP服务器出错:'; +$PHPMAILER_LANG['variable_set'] = '无法设置或重置变量:'; +$PHPMAILER_LANG['extension_missing'] = '丢失模块 Extension:'; diff --git a/kirby/vendor/phpmailer/phpmailer/src/Exception.php b/kirby/vendor/phpmailer/phpmailer/src/Exception.php new file mode 100755 index 0000000..9a05dec --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/src/Exception.php @@ -0,0 +1,39 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2017 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +namespace PHPMailer\PHPMailer; + +/** + * PHPMailer exception handler. + * + * @author Marcus Bointon + */ +class Exception extends \Exception +{ + /** + * Prettify error message output. + * + * @return string + */ + public function errorMessage() + { + return '' . htmlspecialchars($this->getMessage()) . "
\n"; + } +} diff --git a/kirby/vendor/phpmailer/phpmailer/src/OAuth.php b/kirby/vendor/phpmailer/phpmailer/src/OAuth.php new file mode 100755 index 0000000..0bce7e3 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/src/OAuth.php @@ -0,0 +1,138 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2015 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +namespace PHPMailer\PHPMailer; + +use League\OAuth2\Client\Grant\RefreshToken; +use League\OAuth2\Client\Provider\AbstractProvider; +use League\OAuth2\Client\Token\AccessToken; + +/** + * OAuth - OAuth2 authentication wrapper class. + * Uses the oauth2-client package from the League of Extraordinary Packages. + * + * @see http://oauth2-client.thephpleague.com + * + * @author Marcus Bointon (Synchro/coolbru) + */ +class OAuth +{ + /** + * An instance of the League OAuth Client Provider. + * + * @var AbstractProvider + */ + protected $provider; + + /** + * The current OAuth access token. + * + * @var AccessToken + */ + protected $oauthToken; + + /** + * The user's email address, usually used as the login ID + * and also the from address when sending email. + * + * @var string + */ + protected $oauthUserEmail = ''; + + /** + * The client secret, generated in the app definition of the service you're connecting to. + * + * @var string + */ + protected $oauthClientSecret = ''; + + /** + * The client ID, generated in the app definition of the service you're connecting to. + * + * @var string + */ + protected $oauthClientId = ''; + + /** + * The refresh token, used to obtain new AccessTokens. + * + * @var string + */ + protected $oauthRefreshToken = ''; + + /** + * OAuth constructor. + * + * @param array $options Associative array containing + * `provider`, `userName`, `clientSecret`, `clientId` and `refreshToken` elements + */ + public function __construct($options) + { + $this->provider = $options['provider']; + $this->oauthUserEmail = $options['userName']; + $this->oauthClientSecret = $options['clientSecret']; + $this->oauthClientId = $options['clientId']; + $this->oauthRefreshToken = $options['refreshToken']; + } + + /** + * Get a new RefreshToken. + * + * @return RefreshToken + */ + protected function getGrant() + { + return new RefreshToken(); + } + + /** + * Get a new AccessToken. + * + * @return AccessToken + */ + protected function getToken() + { + return $this->provider->getAccessToken( + $this->getGrant(), + ['refresh_token' => $this->oauthRefreshToken] + ); + } + + /** + * Generate a base64-encoded OAuth token. + * + * @return string + */ + public function getOauth64() + { + // Get a new token if it's not available or has expired + if (null === $this->oauthToken or $this->oauthToken->hasExpired()) { + $this->oauthToken = $this->getToken(); + } + + return base64_encode( + 'user=' . + $this->oauthUserEmail . + "\001auth=Bearer " . + $this->oauthToken . + "\001\001" + ); + } +} diff --git a/kirby/vendor/phpmailer/phpmailer/src/PHPMailer.php b/kirby/vendor/phpmailer/phpmailer/src/PHPMailer.php new file mode 100755 index 0000000..5210492 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/src/PHPMailer.php @@ -0,0 +1,4502 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2017 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +namespace PHPMailer\PHPMailer; + +/** + * PHPMailer - PHP email creation and transport class. + * + * @author Marcus Bointon (Synchro/coolbru) + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + */ +class PHPMailer +{ + const CHARSET_ISO88591 = 'iso-8859-1'; + const CHARSET_UTF8 = 'utf-8'; + + const CONTENT_TYPE_PLAINTEXT = 'text/plain'; + const CONTENT_TYPE_TEXT_CALENDAR = 'text/calendar'; + const CONTENT_TYPE_TEXT_HTML = 'text/html'; + const CONTENT_TYPE_MULTIPART_ALTERNATIVE = 'multipart/alternative'; + const CONTENT_TYPE_MULTIPART_MIXED = 'multipart/mixed'; + const CONTENT_TYPE_MULTIPART_RELATED = 'multipart/related'; + + const ENCODING_7BIT = '7bit'; + const ENCODING_8BIT = '8bit'; + const ENCODING_BASE64 = 'base64'; + const ENCODING_BINARY = 'binary'; + const ENCODING_QUOTED_PRINTABLE = 'quoted-printable'; + + /** + * Email priority. + * Options: null (default), 1 = High, 3 = Normal, 5 = low. + * When null, the header is not set at all. + * + * @var int + */ + public $Priority; + + /** + * The character set of the message. + * + * @var string + */ + public $CharSet = self::CHARSET_ISO88591; + + /** + * The MIME Content-type of the message. + * + * @var string + */ + public $ContentType = self::CONTENT_TYPE_PLAINTEXT; + + /** + * The message encoding. + * Options: "8bit", "7bit", "binary", "base64", and "quoted-printable". + * + * @var string + */ + public $Encoding = self::ENCODING_8BIT; + + /** + * Holds the most recent mailer error message. + * + * @var string + */ + public $ErrorInfo = ''; + + /** + * The From email address for the message. + * + * @var string + */ + public $From = 'root@localhost'; + + /** + * The From name of the message. + * + * @var string + */ + public $FromName = 'Root User'; + + /** + * The envelope sender of the message. + * This will usually be turned into a Return-Path header by the receiver, + * and is the address that bounces will be sent to. + * If not empty, will be passed via `-f` to sendmail or as the 'MAIL FROM' value over SMTP. + * + * @var string + */ + public $Sender = ''; + + /** + * The Subject of the message. + * + * @var string + */ + public $Subject = ''; + + /** + * An HTML or plain text message body. + * If HTML then call isHTML(true). + * + * @var string + */ + public $Body = ''; + + /** + * The plain-text message body. + * This body can be read by mail clients that do not have HTML email + * capability such as mutt & Eudora. + * Clients that can read HTML will view the normal Body. + * + * @var string + */ + public $AltBody = ''; + + /** + * An iCal message part body. + * Only supported in simple alt or alt_inline message types + * To generate iCal event structures, use classes like EasyPeasyICS or iCalcreator. + * + * @see http://sprain.ch/blog/downloads/php-class-easypeasyics-create-ical-files-with-php/ + * @see http://kigkonsult.se/iCalcreator/ + * + * @var string + */ + public $Ical = ''; + + /** + * The complete compiled MIME message body. + * + * @var string + */ + protected $MIMEBody = ''; + + /** + * The complete compiled MIME message headers. + * + * @var string + */ + protected $MIMEHeader = ''; + + /** + * Extra headers that createHeader() doesn't fold in. + * + * @var string + */ + protected $mailHeader = ''; + + /** + * Word-wrap the message body to this number of chars. + * Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance. + * + * @see static::STD_LINE_LENGTH + * + * @var int + */ + public $WordWrap = 0; + + /** + * Which method to use to send mail. + * Options: "mail", "sendmail", or "smtp". + * + * @var string + */ + public $Mailer = 'mail'; + + /** + * The path to the sendmail program. + * + * @var string + */ + public $Sendmail = '/usr/sbin/sendmail'; + + /** + * Whether mail() uses a fully sendmail-compatible MTA. + * One which supports sendmail's "-oi -f" options. + * + * @var bool + */ + public $UseSendmailOptions = true; + + /** + * The email address that a reading confirmation should be sent to, also known as read receipt. + * + * @var string + */ + public $ConfirmReadingTo = ''; + + /** + * The hostname to use in the Message-ID header and as default HELO string. + * If empty, PHPMailer attempts to find one with, in order, + * $_SERVER['SERVER_NAME'], gethostname(), php_uname('n'), or the value + * 'localhost.localdomain'. + * + * @var string + */ + public $Hostname = ''; + + /** + * An ID to be used in the Message-ID header. + * If empty, a unique id will be generated. + * You can set your own, but it must be in the format "", + * as defined in RFC5322 section 3.6.4 or it will be ignored. + * + * @see https://tools.ietf.org/html/rfc5322#section-3.6.4 + * + * @var string + */ + public $MessageID = ''; + + /** + * The message Date to be used in the Date header. + * If empty, the current date will be added. + * + * @var string + */ + public $MessageDate = ''; + + /** + * SMTP hosts. + * Either a single hostname or multiple semicolon-delimited hostnames. + * You can also specify a different port + * for each host by using this format: [hostname:port] + * (e.g. "smtp1.example.com:25;smtp2.example.com"). + * You can also specify encryption type, for example: + * (e.g. "tls://smtp1.example.com:587;ssl://smtp2.example.com:465"). + * Hosts will be tried in order. + * + * @var string + */ + public $Host = 'localhost'; + + /** + * The default SMTP server port. + * + * @var int + */ + public $Port = 25; + + /** + * The SMTP HELO of the message. + * Default is $Hostname. If $Hostname is empty, PHPMailer attempts to find + * one with the same method described above for $Hostname. + * + * @see PHPMailer::$Hostname + * + * @var string + */ + public $Helo = ''; + + /** + * What kind of encryption to use on the SMTP connection. + * Options: '', 'ssl' or 'tls'. + * + * @var string + */ + public $SMTPSecure = ''; + + /** + * Whether to enable TLS encryption automatically if a server supports it, + * even if `SMTPSecure` is not set to 'tls'. + * Be aware that in PHP >= 5.6 this requires that the server's certificates are valid. + * + * @var bool + */ + public $SMTPAutoTLS = true; + + /** + * Whether to use SMTP authentication. + * Uses the Username and Password properties. + * + * @see PHPMailer::$Username + * @see PHPMailer::$Password + * + * @var bool + */ + public $SMTPAuth = false; + + /** + * Options array passed to stream_context_create when connecting via SMTP. + * + * @var array + */ + public $SMTPOptions = []; + + /** + * SMTP username. + * + * @var string + */ + public $Username = ''; + + /** + * SMTP password. + * + * @var string + */ + public $Password = ''; + + /** + * SMTP auth type. + * Options are CRAM-MD5, LOGIN, PLAIN, XOAUTH2, attempted in that order if not specified. + * + * @var string + */ + public $AuthType = ''; + + /** + * An instance of the PHPMailer OAuth class. + * + * @var OAuth + */ + protected $oauth; + + /** + * The SMTP server timeout in seconds. + * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2. + * + * @var int + */ + public $Timeout = 300; + + /** + * SMTP class debug output mode. + * Debug output level. + * Options: + * * `0` No output + * * `1` Commands + * * `2` Data and commands + * * `3` As 2 plus connection status + * * `4` Low-level data output. + * + * @see SMTP::$do_debug + * + * @var int + */ + public $SMTPDebug = 0; + + /** + * How to handle debug output. + * Options: + * * `echo` Output plain-text as-is, appropriate for CLI + * * `html` Output escaped, line breaks converted to `
`, appropriate for browser output + * * `error_log` Output to error log as configured in php.ini + * By default PHPMailer will use `echo` if run from a `cli` or `cli-server` SAPI, `html` otherwise. + * Alternatively, you can provide a callable expecting two params: a message string and the debug level: + * + * ```php + * $mail->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";}; + * ``` + * + * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug` + * level output is used: + * + * ```php + * $mail->Debugoutput = new myPsr3Logger; + * ``` + * + * @see SMTP::$Debugoutput + * + * @var string|callable|\Psr\Log\LoggerInterface + */ + public $Debugoutput = 'echo'; + + /** + * Whether to keep SMTP connection open after each message. + * If this is set to true then to close the connection + * requires an explicit call to smtpClose(). + * + * @var bool + */ + public $SMTPKeepAlive = false; + + /** + * Whether to split multiple to addresses into multiple messages + * or send them all in one message. + * Only supported in `mail` and `sendmail` transports, not in SMTP. + * + * @var bool + */ + public $SingleTo = false; + + /** + * Storage for addresses when SingleTo is enabled. + * + * @var array + */ + protected $SingleToArray = []; + + /** + * Whether to generate VERP addresses on send. + * Only applicable when sending via SMTP. + * + * @see https://en.wikipedia.org/wiki/Variable_envelope_return_path + * @see http://www.postfix.org/VERP_README.html Postfix VERP info + * + * @var bool + */ + public $do_verp = false; + + /** + * Whether to allow sending messages with an empty body. + * + * @var bool + */ + public $AllowEmpty = false; + + /** + * DKIM selector. + * + * @var string + */ + public $DKIM_selector = ''; + + /** + * DKIM Identity. + * Usually the email address used as the source of the email. + * + * @var string + */ + public $DKIM_identity = ''; + + /** + * DKIM passphrase. + * Used if your key is encrypted. + * + * @var string + */ + public $DKIM_passphrase = ''; + + /** + * DKIM signing domain name. + * + * @example 'example.com' + * + * @var string + */ + public $DKIM_domain = ''; + + /** + * DKIM Copy header field values for diagnostic use. + * + * @var bool + */ + public $DKIM_copyHeaderFields = true; + + /** + * DKIM Extra signing headers. + * + * @example ['List-Unsubscribe', 'List-Help'] + * + * @var array + */ + public $DKIM_extraHeaders = []; + + /** + * DKIM private key file path. + * + * @var string + */ + public $DKIM_private = ''; + + /** + * DKIM private key string. + * + * If set, takes precedence over `$DKIM_private`. + * + * @var string + */ + public $DKIM_private_string = ''; + + /** + * Callback Action function name. + * + * The function that handles the result of the send email action. + * It is called out by send() for each email sent. + * + * Value can be any php callable: http://www.php.net/is_callable + * + * Parameters: + * bool $result result of the send action + * array $to email addresses of the recipients + * array $cc cc email addresses + * array $bcc bcc email addresses + * string $subject the subject + * string $body the email body + * string $from email address of sender + * string $extra extra information of possible use + * "smtp_transaction_id' => last smtp transaction id + * + * @var string + */ + public $action_function = ''; + + /** + * What to put in the X-Mailer header. + * Options: An empty string for PHPMailer default, whitespace for none, or a string to use. + * + * @var string + */ + public $XMailer = ''; + + /** + * Which validator to use by default when validating email addresses. + * May be a callable to inject your own validator, but there are several built-in validators. + * The default validator uses PHP's FILTER_VALIDATE_EMAIL filter_var option. + * + * @see PHPMailer::validateAddress() + * + * @var string|callable + */ + public static $validator = 'php'; + + /** + * An instance of the SMTP sender class. + * + * @var SMTP + */ + protected $smtp; + + /** + * The array of 'to' names and addresses. + * + * @var array + */ + protected $to = []; + + /** + * The array of 'cc' names and addresses. + * + * @var array + */ + protected $cc = []; + + /** + * The array of 'bcc' names and addresses. + * + * @var array + */ + protected $bcc = []; + + /** + * The array of reply-to names and addresses. + * + * @var array + */ + protected $ReplyTo = []; + + /** + * An array of all kinds of addresses. + * Includes all of $to, $cc, $bcc. + * + * @see PHPMailer::$to + * @see PHPMailer::$cc + * @see PHPMailer::$bcc + * + * @var array + */ + protected $all_recipients = []; + + /** + * An array of names and addresses queued for validation. + * In send(), valid and non duplicate entries are moved to $all_recipients + * and one of $to, $cc, or $bcc. + * This array is used only for addresses with IDN. + * + * @see PHPMailer::$to + * @see PHPMailer::$cc + * @see PHPMailer::$bcc + * @see PHPMailer::$all_recipients + * + * @var array + */ + protected $RecipientsQueue = []; + + /** + * An array of reply-to names and addresses queued for validation. + * In send(), valid and non duplicate entries are moved to $ReplyTo. + * This array is used only for addresses with IDN. + * + * @see PHPMailer::$ReplyTo + * + * @var array + */ + protected $ReplyToQueue = []; + + /** + * The array of attachments. + * + * @var array + */ + protected $attachment = []; + + /** + * The array of custom headers. + * + * @var array + */ + protected $CustomHeader = []; + + /** + * The most recent Message-ID (including angular brackets). + * + * @var string + */ + protected $lastMessageID = ''; + + /** + * The message's MIME type. + * + * @var string + */ + protected $message_type = ''; + + /** + * The array of MIME boundary strings. + * + * @var array + */ + protected $boundary = []; + + /** + * The array of available languages. + * + * @var array + */ + protected $language = []; + + /** + * The number of errors encountered. + * + * @var int + */ + protected $error_count = 0; + + /** + * The S/MIME certificate file path. + * + * @var string + */ + protected $sign_cert_file = ''; + + /** + * The S/MIME key file path. + * + * @var string + */ + protected $sign_key_file = ''; + + /** + * The optional S/MIME extra certificates ("CA Chain") file path. + * + * @var string + */ + protected $sign_extracerts_file = ''; + + /** + * The S/MIME password for the key. + * Used only if the key is encrypted. + * + * @var string + */ + protected $sign_key_pass = ''; + + /** + * Whether to throw exceptions for errors. + * + * @var bool + */ + protected $exceptions = false; + + /** + * Unique ID used for message ID and boundaries. + * + * @var string + */ + protected $uniqueid = ''; + + /** + * The PHPMailer Version number. + * + * @var string + */ + const VERSION = '6.0.7'; + + /** + * Error severity: message only, continue processing. + * + * @var int + */ + const STOP_MESSAGE = 0; + + /** + * Error severity: message, likely ok to continue processing. + * + * @var int + */ + const STOP_CONTINUE = 1; + + /** + * Error severity: message, plus full stop, critical error reached. + * + * @var int + */ + const STOP_CRITICAL = 2; + + /** + * SMTP RFC standard line ending. + * + * @var string + */ + protected static $LE = "\r\n"; + + /** + * The maximum line length allowed by RFC 2822 section 2.1.1. + * + * @var int + */ + const MAX_LINE_LENGTH = 998; + + /** + * The lower maximum line length allowed by RFC 2822 section 2.1.1. + * This length does NOT include the line break + * 76 means that lines will be 77 or 78 chars depending on whether + * the line break format is LF or CRLF; both are valid. + * + * @var int + */ + const STD_LINE_LENGTH = 76; + + /** + * Constructor. + * + * @param bool $exceptions Should we throw external exceptions? + */ + public function __construct($exceptions = null) + { + if (null !== $exceptions) { + $this->exceptions = (bool) $exceptions; + } + //Pick an appropriate debug output format automatically + $this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html'); + } + + /** + * Destructor. + */ + public function __destruct() + { + //Close any open SMTP connection nicely + $this->smtpClose(); + } + + /** + * Call mail() in a safe_mode-aware fashion. + * Also, unless sendmail_path points to sendmail (or something that + * claims to be sendmail), don't pass params (not a perfect fix, + * but it will do). + * + * @param string $to To + * @param string $subject Subject + * @param string $body Message Body + * @param string $header Additional Header(s) + * @param string|null $params Params + * + * @return bool + */ + private function mailPassthru($to, $subject, $body, $header, $params) + { + //Check overloading of mail function to avoid double-encoding + if (ini_get('mbstring.func_overload') & 1) { + $subject = $this->secureHeader($subject); + } else { + $subject = $this->encodeHeader($this->secureHeader($subject)); + } + //Calling mail() with null params breaks + if (!$this->UseSendmailOptions or null === $params) { + $result = @mail($to, $subject, $body, $header); + } else { + $result = @mail($to, $subject, $body, $header, $params); + } + + return $result; + } + + /** + * Output debugging info via user-defined method. + * Only generates output if SMTP debug output is enabled (@see SMTP::$do_debug). + * + * @see PHPMailer::$Debugoutput + * @see PHPMailer::$SMTPDebug + * + * @param string $str + */ + protected function edebug($str) + { + if ($this->SMTPDebug <= 0) { + return; + } + //Is this a PSR-3 logger? + if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) { + $this->Debugoutput->debug($str); + + return; + } + //Avoid clash with built-in function names + if (!in_array($this->Debugoutput, ['error_log', 'html', 'echo']) and is_callable($this->Debugoutput)) { + call_user_func($this->Debugoutput, $str, $this->SMTPDebug); + + return; + } + switch ($this->Debugoutput) { + case 'error_log': + //Don't output, just log + error_log($str); + break; + case 'html': + //Cleans up output a bit for a better looking, HTML-safe output + echo htmlentities( + preg_replace('/[\r\n]+/', '', $str), + ENT_QUOTES, + 'UTF-8' + ), "
\n"; + break; + case 'echo': + default: + //Normalize line breaks + $str = preg_replace('/\r\n|\r/ms', "\n", $str); + echo gmdate('Y-m-d H:i:s'), + "\t", + //Trim trailing space + trim( + //Indent for readability, except for trailing break + str_replace( + "\n", + "\n \t ", + trim($str) + ) + ), + "\n"; + } + } + + /** + * Sets message type to HTML or plain. + * + * @param bool $isHtml True for HTML mode + */ + public function isHTML($isHtml = true) + { + if ($isHtml) { + $this->ContentType = static::CONTENT_TYPE_TEXT_HTML; + } else { + $this->ContentType = static::CONTENT_TYPE_PLAINTEXT; + } + } + + /** + * Send messages using SMTP. + */ + public function isSMTP() + { + $this->Mailer = 'smtp'; + } + + /** + * Send messages using PHP's mail() function. + */ + public function isMail() + { + $this->Mailer = 'mail'; + } + + /** + * Send messages using $Sendmail. + */ + public function isSendmail() + { + $ini_sendmail_path = ini_get('sendmail_path'); + + if (false === stripos($ini_sendmail_path, 'sendmail')) { + $this->Sendmail = '/usr/sbin/sendmail'; + } else { + $this->Sendmail = $ini_sendmail_path; + } + $this->Mailer = 'sendmail'; + } + + /** + * Send messages using qmail. + */ + public function isQmail() + { + $ini_sendmail_path = ini_get('sendmail_path'); + + if (false === stripos($ini_sendmail_path, 'qmail')) { + $this->Sendmail = '/var/qmail/bin/qmail-inject'; + } else { + $this->Sendmail = $ini_sendmail_path; + } + $this->Mailer = 'qmail'; + } + + /** + * Add a "To" address. + * + * @param string $address The email address to send to + * @param string $name + * + * @return bool true on success, false if address already used or invalid in some way + */ + public function addAddress($address, $name = '') + { + return $this->addOrEnqueueAnAddress('to', $address, $name); + } + + /** + * Add a "CC" address. + * + * @param string $address The email address to send to + * @param string $name + * + * @return bool true on success, false if address already used or invalid in some way + */ + public function addCC($address, $name = '') + { + return $this->addOrEnqueueAnAddress('cc', $address, $name); + } + + /** + * Add a "BCC" address. + * + * @param string $address The email address to send to + * @param string $name + * + * @return bool true on success, false if address already used or invalid in some way + */ + public function addBCC($address, $name = '') + { + return $this->addOrEnqueueAnAddress('bcc', $address, $name); + } + + /** + * Add a "Reply-To" address. + * + * @param string $address The email address to reply to + * @param string $name + * + * @return bool true on success, false if address already used or invalid in some way + */ + public function addReplyTo($address, $name = '') + { + return $this->addOrEnqueueAnAddress('Reply-To', $address, $name); + } + + /** + * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer + * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still + * be modified after calling this function), addition of such addresses is delayed until send(). + * Addresses that have been added already return false, but do not throw exceptions. + * + * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' + * @param string $address The email address to send, resp. to reply to + * @param string $name + * + * @throws Exception + * + * @return bool true on success, false if address already used or invalid in some way + */ + protected function addOrEnqueueAnAddress($kind, $address, $name) + { + $address = trim($address); + $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim + $pos = strrpos($address, '@'); + if (false === $pos) { + // At-sign is missing. + $error_message = sprintf('%s (%s): %s', + $this->lang('invalid_address'), + $kind, + $address); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + $params = [$kind, $address, $name]; + // Enqueue addresses with IDN until we know the PHPMailer::$CharSet. + if ($this->has8bitChars(substr($address, ++$pos)) and static::idnSupported()) { + if ('Reply-To' != $kind) { + if (!array_key_exists($address, $this->RecipientsQueue)) { + $this->RecipientsQueue[$address] = $params; + + return true; + } + } else { + if (!array_key_exists($address, $this->ReplyToQueue)) { + $this->ReplyToQueue[$address] = $params; + + return true; + } + } + + return false; + } + + // Immediately add standard addresses without IDN. + return call_user_func_array([$this, 'addAnAddress'], $params); + } + + /** + * Add an address to one of the recipient arrays or to the ReplyTo array. + * Addresses that have been added already return false, but do not throw exceptions. + * + * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' + * @param string $address The email address to send, resp. to reply to + * @param string $name + * + * @throws Exception + * + * @return bool true on success, false if address already used or invalid in some way + */ + protected function addAnAddress($kind, $address, $name = '') + { + if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) { + $error_message = sprintf('%s: %s', + $this->lang('Invalid recipient kind'), + $kind); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + if (!static::validateAddress($address)) { + $error_message = sprintf('%s (%s): %s', + $this->lang('invalid_address'), + $kind, + $address); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + if ('Reply-To' != $kind) { + if (!array_key_exists(strtolower($address), $this->all_recipients)) { + $this->{$kind}[] = [$address, $name]; + $this->all_recipients[strtolower($address)] = true; + + return true; + } + } else { + if (!array_key_exists(strtolower($address), $this->ReplyTo)) { + $this->ReplyTo[strtolower($address)] = [$address, $name]; + + return true; + } + } + + return false; + } + + /** + * Parse and validate a string containing one or more RFC822-style comma-separated email addresses + * of the form "display name
" into an array of name/address pairs. + * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available. + * Note that quotes in the name part are removed. + * + * @see http://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation + * + * @param string $addrstr The address list string + * @param bool $useimap Whether to use the IMAP extension to parse the list + * + * @return array + */ + public static function parseAddresses($addrstr, $useimap = true) + { + $addresses = []; + if ($useimap and function_exists('imap_rfc822_parse_adrlist')) { + //Use this built-in parser if it's available + $list = imap_rfc822_parse_adrlist($addrstr, ''); + foreach ($list as $address) { + if ('.SYNTAX-ERROR.' != $address->host) { + if (static::validateAddress($address->mailbox . '@' . $address->host)) { + $addresses[] = [ + 'name' => (property_exists($address, 'personal') ? $address->personal : ''), + 'address' => $address->mailbox . '@' . $address->host, + ]; + } + } + } + } else { + //Use this simpler parser + $list = explode(',', $addrstr); + foreach ($list as $address) { + $address = trim($address); + //Is there a separate name part? + if (strpos($address, '<') === false) { + //No separate name, just use the whole thing + if (static::validateAddress($address)) { + $addresses[] = [ + 'name' => '', + 'address' => $address, + ]; + } + } else { + list($name, $email) = explode('<', $address); + $email = trim(str_replace('>', '', $email)); + if (static::validateAddress($email)) { + $addresses[] = [ + 'name' => trim(str_replace(['"', "'"], '', $name)), + 'address' => $email, + ]; + } + } + } + } + + return $addresses; + } + + /** + * Set the From and FromName properties. + * + * @param string $address + * @param string $name + * @param bool $auto Whether to also set the Sender address, defaults to true + * + * @throws Exception + * + * @return bool + */ + public function setFrom($address, $name = '', $auto = true) + { + $address = trim($address); + $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim + // Don't validate now addresses with IDN. Will be done in send(). + $pos = strrpos($address, '@'); + if (false === $pos or + (!$this->has8bitChars(substr($address, ++$pos)) or !static::idnSupported()) and + !static::validateAddress($address)) { + $error_message = sprintf('%s (From): %s', + $this->lang('invalid_address'), + $address); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + $this->From = $address; + $this->FromName = $name; + if ($auto) { + if (empty($this->Sender)) { + $this->Sender = $address; + } + } + + return true; + } + + /** + * Return the Message-ID header of the last email. + * Technically this is the value from the last time the headers were created, + * but it's also the message ID of the last sent message except in + * pathological cases. + * + * @return string + */ + public function getLastMessageID() + { + return $this->lastMessageID; + } + + /** + * Check that a string looks like an email address. + * Validation patterns supported: + * * `auto` Pick best pattern automatically; + * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0; + * * `pcre` Use old PCRE implementation; + * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL; + * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements. + * * `noregex` Don't use a regex: super fast, really dumb. + * Alternatively you may pass in a callable to inject your own validator, for example: + * + * ```php + * PHPMailer::validateAddress('user@example.com', function($address) { + * return (strpos($address, '@') !== false); + * }); + * ``` + * + * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator. + * + * @param string $address The email address to check + * @param string|callable $patternselect Which pattern to use + * + * @return bool + */ + public static function validateAddress($address, $patternselect = null) + { + if (null === $patternselect) { + $patternselect = static::$validator; + } + if (is_callable($patternselect)) { + return call_user_func($patternselect, $address); + } + //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321 + if (strpos($address, "\n") !== false or strpos($address, "\r") !== false) { + return false; + } + switch ($patternselect) { + case 'pcre': //Kept for BC + case 'pcre8': + /* + * A more complex and more permissive version of the RFC5322 regex on which FILTER_VALIDATE_EMAIL + * is based. + * In addition to the addresses allowed by filter_var, also permits: + * * dotless domains: `a@b` + * * comments: `1234 @ local(blah) .machine .example` + * * quoted elements: `'"test blah"@example.org'` + * * numeric TLDs: `a@b.123` + * * unbracketed IPv4 literals: `a@192.168.0.1` + * * IPv6 literals: 'first.last@[IPv6:a1::]' + * Not all of these will necessarily work for sending! + * + * @see http://squiloople.com/2009/12/20/email-address-validation/ + * @copyright 2009-2010 Michael Rushton + * Feel free to use and redistribute this code. But please keep this copyright notice. + */ + return (bool) preg_match( + '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' . + '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' . + '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' . + '([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' . + '(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' . + '(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' . + '|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' . + '|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' . + '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD', + $address + ); + case 'html5': + /* + * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements. + * + * @see http://www.whatwg.org/specs/web-apps/current-work/#e-mail-state-(type=email) + */ + return (bool) preg_match( + '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' . + '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD', + $address + ); + case 'php': + default: + return (bool) filter_var($address, FILTER_VALIDATE_EMAIL); + } + } + + /** + * Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the + * `intl` and `mbstring` PHP extensions. + * + * @return bool `true` if required functions for IDN support are present + */ + public static function idnSupported() + { + return function_exists('idn_to_ascii') and function_exists('mb_convert_encoding'); + } + + /** + * Converts IDN in given email address to its ASCII form, also known as punycode, if possible. + * Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet. + * This function silently returns unmodified address if: + * - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form) + * - Conversion to punycode is impossible (e.g. required PHP functions are not available) + * or fails for any reason (e.g. domain contains characters not allowed in an IDN). + * + * @see PHPMailer::$CharSet + * + * @param string $address The email address to convert + * + * @return string The encoded address in ASCII form + */ + public function punyencodeAddress($address) + { + // Verify we have required functions, CharSet, and at-sign. + $pos = strrpos($address, '@'); + if (static::idnSupported() and + !empty($this->CharSet) and + false !== $pos + ) { + $domain = substr($address, ++$pos); + // Verify CharSet string is a valid one, and domain properly encoded in this CharSet. + if ($this->has8bitChars($domain) and @mb_check_encoding($domain, $this->CharSet)) { + $domain = mb_convert_encoding($domain, 'UTF-8', $this->CharSet); + //Ignore IDE complaints about this line - method signature changed in PHP 5.4 + $errorcode = 0; + $punycode = idn_to_ascii($domain, $errorcode, INTL_IDNA_VARIANT_UTS46); + if (false !== $punycode) { + return substr($address, 0, $pos) . $punycode; + } + } + } + + return $address; + } + + /** + * Create a message and send it. + * Uses the sending method specified by $Mailer. + * + * @throws Exception + * + * @return bool false on error - See the ErrorInfo property for details of the error + */ + public function send() + { + try { + if (!$this->preSend()) { + return false; + } + + return $this->postSend(); + } catch (Exception $exc) { + $this->mailHeader = ''; + $this->setError($exc->getMessage()); + if ($this->exceptions) { + throw $exc; + } + + return false; + } + } + + /** + * Prepare a message for sending. + * + * @throws Exception + * + * @return bool + */ + public function preSend() + { + if ('smtp' == $this->Mailer or + ('mail' == $this->Mailer and stripos(PHP_OS, 'WIN') === 0) + ) { + //SMTP mandates RFC-compliant line endings + //and it's also used with mail() on Windows + static::setLE("\r\n"); + } else { + //Maintain backward compatibility with legacy Linux command line mailers + static::setLE(PHP_EOL); + } + //Check for buggy PHP versions that add a header with an incorrect line break + if (ini_get('mail.add_x_header') == 1 + and 'mail' == $this->Mailer + and stripos(PHP_OS, 'WIN') === 0 + and ((version_compare(PHP_VERSION, '7.0.0', '>=') + and version_compare(PHP_VERSION, '7.0.17', '<')) + or (version_compare(PHP_VERSION, '7.1.0', '>=') + and version_compare(PHP_VERSION, '7.1.3', '<'))) + ) { + trigger_error( + 'Your version of PHP is affected by a bug that may result in corrupted messages.' . + ' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' . + ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.', + E_USER_WARNING + ); + } + + try { + $this->error_count = 0; // Reset errors + $this->mailHeader = ''; + + // Dequeue recipient and Reply-To addresses with IDN + foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) { + $params[1] = $this->punyencodeAddress($params[1]); + call_user_func_array([$this, 'addAnAddress'], $params); + } + if (count($this->to) + count($this->cc) + count($this->bcc) < 1) { + throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL); + } + + // Validate From, Sender, and ConfirmReadingTo addresses + foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) { + $this->$address_kind = trim($this->$address_kind); + if (empty($this->$address_kind)) { + continue; + } + $this->$address_kind = $this->punyencodeAddress($this->$address_kind); + if (!static::validateAddress($this->$address_kind)) { + $error_message = sprintf('%s (%s): %s', + $this->lang('invalid_address'), + $address_kind, + $this->$address_kind); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + } + + // Set whether the message is multipart/alternative + if ($this->alternativeExists()) { + $this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE; + } + + $this->setMessageType(); + // Refuse to send an empty message unless we are specifically allowing it + if (!$this->AllowEmpty and empty($this->Body)) { + throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL); + } + + //Trim subject consistently + $this->Subject = trim($this->Subject); + // Create body before headers in case body makes changes to headers (e.g. altering transfer encoding) + $this->MIMEHeader = ''; + $this->MIMEBody = $this->createBody(); + // createBody may have added some headers, so retain them + $tempheaders = $this->MIMEHeader; + $this->MIMEHeader = $this->createHeader(); + $this->MIMEHeader .= $tempheaders; + + // To capture the complete message when using mail(), create + // an extra header list which createHeader() doesn't fold in + if ('mail' == $this->Mailer) { + if (count($this->to) > 0) { + $this->mailHeader .= $this->addrAppend('To', $this->to); + } else { + $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;'); + } + $this->mailHeader .= $this->headerLine( + 'Subject', + $this->encodeHeader($this->secureHeader($this->Subject)) + ); + } + + // Sign with DKIM if enabled + if (!empty($this->DKIM_domain) + and !empty($this->DKIM_selector) + and (!empty($this->DKIM_private_string) + or (!empty($this->DKIM_private) + and static::isPermittedPath($this->DKIM_private) + and file_exists($this->DKIM_private) + ) + ) + ) { + $header_dkim = $this->DKIM_Add( + $this->MIMEHeader . $this->mailHeader, + $this->encodeHeader($this->secureHeader($this->Subject)), + $this->MIMEBody + ); + $this->MIMEHeader = rtrim($this->MIMEHeader, "\r\n ") . static::$LE . + static::normalizeBreaks($header_dkim) . static::$LE; + } + + return true; + } catch (Exception $exc) { + $this->setError($exc->getMessage()); + if ($this->exceptions) { + throw $exc; + } + + return false; + } + } + + /** + * Actually send a message via the selected mechanism. + * + * @throws Exception + * + * @return bool + */ + public function postSend() + { + try { + // Choose the mailer and send through it + switch ($this->Mailer) { + case 'sendmail': + case 'qmail': + return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody); + case 'smtp': + return $this->smtpSend($this->MIMEHeader, $this->MIMEBody); + case 'mail': + return $this->mailSend($this->MIMEHeader, $this->MIMEBody); + default: + $sendMethod = $this->Mailer . 'Send'; + if (method_exists($this, $sendMethod)) { + return $this->$sendMethod($this->MIMEHeader, $this->MIMEBody); + } + + return $this->mailSend($this->MIMEHeader, $this->MIMEBody); + } + } catch (Exception $exc) { + $this->setError($exc->getMessage()); + $this->edebug($exc->getMessage()); + if ($this->exceptions) { + throw $exc; + } + } + + return false; + } + + /** + * Send mail using the $Sendmail program. + * + * @see PHPMailer::$Sendmail + * + * @param string $header The message headers + * @param string $body The message body + * + * @throws Exception + * + * @return bool + */ + protected function sendmailSend($header, $body) + { + // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. + if (!empty($this->Sender) and self::isShellSafe($this->Sender)) { + if ('qmail' == $this->Mailer) { + $sendmailFmt = '%s -f%s'; + } else { + $sendmailFmt = '%s -oi -f%s -t'; + } + } else { + if ('qmail' == $this->Mailer) { + $sendmailFmt = '%s'; + } else { + $sendmailFmt = '%s -oi -t'; + } + } + + $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender); + + if ($this->SingleTo) { + foreach ($this->SingleToArray as $toAddr) { + $mail = @popen($sendmail, 'w'); + if (!$mail) { + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + fwrite($mail, 'To: ' . $toAddr . "\n"); + fwrite($mail, $header); + fwrite($mail, $body); + $result = pclose($mail); + $this->doCallback( + ($result == 0), + [$toAddr], + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From, + [] + ); + if (0 !== $result) { + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + } + } else { + $mail = @popen($sendmail, 'w'); + if (!$mail) { + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + fwrite($mail, $header); + fwrite($mail, $body); + $result = pclose($mail); + $this->doCallback( + ($result == 0), + $this->to, + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From, + [] + ); + if (0 !== $result) { + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + } + + return true; + } + + /** + * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters. + * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows. + * + * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report + * + * @param string $string The string to be validated + * + * @return bool + */ + protected static function isShellSafe($string) + { + // Future-proof + if (escapeshellcmd($string) !== $string + or !in_array(escapeshellarg($string), ["'$string'", "\"$string\""]) + ) { + return false; + } + + $length = strlen($string); + + for ($i = 0; $i < $length; ++$i) { + $c = $string[$i]; + + // All other characters have a special meaning in at least one common shell, including = and +. + // Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here. + // Note that this does permit non-Latin alphanumeric characters based on the current locale. + if (!ctype_alnum($c) && strpos('@_-.', $c) === false) { + return false; + } + } + + return true; + } + + /** + * Check whether a file path is of a permitted type. + * Used to reject URLs and phar files from functions that access local file paths, + * such as addAttachment. + * + * @param string $path A relative or absolute path to a file + * + * @return bool + */ + protected static function isPermittedPath($path) + { + return !preg_match('#^[a-z]+://#i', $path); + } + + /** + * Send mail using the PHP mail() function. + * + * @see http://www.php.net/manual/en/book.mail.php + * + * @param string $header The message headers + * @param string $body The message body + * + * @throws Exception + * + * @return bool + */ + protected function mailSend($header, $body) + { + $toArr = []; + foreach ($this->to as $toaddr) { + $toArr[] = $this->addrFormat($toaddr); + } + $to = implode(', ', $toArr); + + $params = null; + //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver + if (!empty($this->Sender) and static::validateAddress($this->Sender)) { + //A space after `-f` is optional, but there is a long history of its presence + //causing problems, so we don't use one + //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html + //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html + //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html + //Example problem: https://www.drupal.org/node/1057954 + // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. + if (self::isShellSafe($this->Sender)) { + $params = sprintf('-f%s', $this->Sender); + } + } + if (!empty($this->Sender) and static::validateAddress($this->Sender)) { + $old_from = ini_get('sendmail_from'); + ini_set('sendmail_from', $this->Sender); + } + $result = false; + if ($this->SingleTo and count($toArr) > 1) { + foreach ($toArr as $toAddr) { + $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params); + $this->doCallback($result, [$toAddr], $this->cc, $this->bcc, $this->Subject, $body, $this->From, []); + } + } else { + $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params); + $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From, []); + } + if (isset($old_from)) { + ini_set('sendmail_from', $old_from); + } + if (!$result) { + throw new Exception($this->lang('instantiate'), self::STOP_CRITICAL); + } + + return true; + } + + /** + * Get an instance to use for SMTP operations. + * Override this function to load your own SMTP implementation, + * or set one with setSMTPInstance. + * + * @return SMTP + */ + public function getSMTPInstance() + { + if (!is_object($this->smtp)) { + $this->smtp = new SMTP(); + } + + return $this->smtp; + } + + /** + * Provide an instance to use for SMTP operations. + * + * @param SMTP $smtp + * + * @return SMTP + */ + public function setSMTPInstance(SMTP $smtp) + { + $this->smtp = $smtp; + + return $this->smtp; + } + + /** + * Send mail via SMTP. + * Returns false if there is a bad MAIL FROM, RCPT, or DATA input. + * + * @see PHPMailer::setSMTPInstance() to use a different class. + * + * @uses \PHPMailer\PHPMailer\SMTP + * + * @param string $header The message headers + * @param string $body The message body + * + * @throws Exception + * + * @return bool + */ + protected function smtpSend($header, $body) + { + $bad_rcpt = []; + if (!$this->smtpConnect($this->SMTPOptions)) { + throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL); + } + //Sender already validated in preSend() + if ('' == $this->Sender) { + $smtp_from = $this->From; + } else { + $smtp_from = $this->Sender; + } + if (!$this->smtp->mail($smtp_from)) { + $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError())); + throw new Exception($this->ErrorInfo, self::STOP_CRITICAL); + } + + $callbacks = []; + // Attempt to send to all recipients + foreach ([$this->to, $this->cc, $this->bcc] as $togroup) { + foreach ($togroup as $to) { + if (!$this->smtp->recipient($to[0])) { + $error = $this->smtp->getError(); + $bad_rcpt[] = ['to' => $to[0], 'error' => $error['detail']]; + $isSent = false; + } else { + $isSent = true; + } + + $callbacks[] = ['issent'=>$isSent, 'to'=>$to[0]]; + } + } + + // Only send the DATA command if we have viable recipients + if ((count($this->all_recipients) > count($bad_rcpt)) and !$this->smtp->data($header . $body)) { + throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL); + } + + $smtp_transaction_id = $this->smtp->getLastTransactionID(); + + if ($this->SMTPKeepAlive) { + $this->smtp->reset(); + } else { + $this->smtp->quit(); + $this->smtp->close(); + } + + foreach ($callbacks as $cb) { + $this->doCallback( + $cb['issent'], + [$cb['to']], + [], + [], + $this->Subject, + $body, + $this->From, + ['smtp_transaction_id' => $smtp_transaction_id] + ); + } + + //Create error message for any bad addresses + if (count($bad_rcpt) > 0) { + $errstr = ''; + foreach ($bad_rcpt as $bad) { + $errstr .= $bad['to'] . ': ' . $bad['error']; + } + throw new Exception( + $this->lang('recipients_failed') . $errstr, + self::STOP_CONTINUE + ); + } + + return true; + } + + /** + * Initiate a connection to an SMTP server. + * Returns false if the operation failed. + * + * @param array $options An array of options compatible with stream_context_create() + * + * @throws Exception + * + * @uses \PHPMailer\PHPMailer\SMTP + * + * @return bool + */ + public function smtpConnect($options = null) + { + if (null === $this->smtp) { + $this->smtp = $this->getSMTPInstance(); + } + + //If no options are provided, use whatever is set in the instance + if (null === $options) { + $options = $this->SMTPOptions; + } + + // Already connected? + if ($this->smtp->connected()) { + return true; + } + + $this->smtp->setTimeout($this->Timeout); + $this->smtp->setDebugLevel($this->SMTPDebug); + $this->smtp->setDebugOutput($this->Debugoutput); + $this->smtp->setVerp($this->do_verp); + $hosts = explode(';', $this->Host); + $lastexception = null; + + foreach ($hosts as $hostentry) { + $hostinfo = []; + if (!preg_match( + '/^((ssl|tls):\/\/)*([a-zA-Z0-9\.-]*|\[[a-fA-F0-9:]+\]):?([0-9]*)$/', + trim($hostentry), + $hostinfo + )) { + static::edebug($this->lang('connect_host') . ' ' . $hostentry); + // Not a valid host entry + continue; + } + // $hostinfo[2]: optional ssl or tls prefix + // $hostinfo[3]: the hostname + // $hostinfo[4]: optional port number + // The host string prefix can temporarily override the current setting for SMTPSecure + // If it's not specified, the default value is used + + //Check the host name is a valid name or IP address before trying to use it + if (!static::isValidHost($hostinfo[3])) { + static::edebug($this->lang('connect_host') . ' ' . $hostentry); + continue; + } + $prefix = ''; + $secure = $this->SMTPSecure; + $tls = ('tls' == $this->SMTPSecure); + if ('ssl' == $hostinfo[2] or ('' == $hostinfo[2] and 'ssl' == $this->SMTPSecure)) { + $prefix = 'ssl://'; + $tls = false; // Can't have SSL and TLS at the same time + $secure = 'ssl'; + } elseif ('tls' == $hostinfo[2]) { + $tls = true; + // tls doesn't use a prefix + $secure = 'tls'; + } + //Do we need the OpenSSL extension? + $sslext = defined('OPENSSL_ALGO_SHA256'); + if ('tls' === $secure or 'ssl' === $secure) { + //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled + if (!$sslext) { + throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL); + } + } + $host = $hostinfo[3]; + $port = $this->Port; + $tport = (int) $hostinfo[4]; + if ($tport > 0 and $tport < 65536) { + $port = $tport; + } + if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) { + try { + if ($this->Helo) { + $hello = $this->Helo; + } else { + $hello = $this->serverHostname(); + } + $this->smtp->hello($hello); + //Automatically enable TLS encryption if: + // * it's not disabled + // * we have openssl extension + // * we are not already using SSL + // * the server offers STARTTLS + if ($this->SMTPAutoTLS and $sslext and 'ssl' != $secure and $this->smtp->getServerExt('STARTTLS')) { + $tls = true; + } + if ($tls) { + if (!$this->smtp->startTLS()) { + throw new Exception($this->lang('connect_host')); + } + // We must resend EHLO after TLS negotiation + $this->smtp->hello($hello); + } + if ($this->SMTPAuth) { + if (!$this->smtp->authenticate( + $this->Username, + $this->Password, + $this->AuthType, + $this->oauth + ) + ) { + throw new Exception($this->lang('authenticate')); + } + } + + return true; + } catch (Exception $exc) { + $lastexception = $exc; + $this->edebug($exc->getMessage()); + // We must have connected, but then failed TLS or Auth, so close connection nicely + $this->smtp->quit(); + } + } + } + // If we get here, all connection attempts have failed, so close connection hard + $this->smtp->close(); + // As we've caught all exceptions, just report whatever the last one was + if ($this->exceptions and null !== $lastexception) { + throw $lastexception; + } + + return false; + } + + /** + * Close the active SMTP session if one exists. + */ + public function smtpClose() + { + if (null !== $this->smtp) { + if ($this->smtp->connected()) { + $this->smtp->quit(); + $this->smtp->close(); + } + } + } + + /** + * Set the language for error messages. + * Returns false if it cannot load the language file. + * The default language is English. + * + * @param string $langcode ISO 639-1 2-character language code (e.g. French is "fr") + * @param string $lang_path Path to the language file directory, with trailing separator (slash) + * + * @return bool + */ + public function setLanguage($langcode = 'en', $lang_path = '') + { + // Backwards compatibility for renamed language codes + $renamed_langcodes = [ + 'br' => 'pt_br', + 'cz' => 'cs', + 'dk' => 'da', + 'no' => 'nb', + 'se' => 'sv', + 'rs' => 'sr', + 'tg' => 'tl', + ]; + + if (isset($renamed_langcodes[$langcode])) { + $langcode = $renamed_langcodes[$langcode]; + } + + // Define full set of translatable strings in English + $PHPMAILER_LANG = [ + 'authenticate' => 'SMTP Error: Could not authenticate.', + 'connect_host' => 'SMTP Error: Could not connect to SMTP host.', + 'data_not_accepted' => 'SMTP Error: data not accepted.', + 'empty_message' => 'Message body empty', + 'encoding' => 'Unknown encoding: ', + 'execute' => 'Could not execute: ', + 'file_access' => 'Could not access file: ', + 'file_open' => 'File Error: Could not open file: ', + 'from_failed' => 'The following From address failed: ', + 'instantiate' => 'Could not instantiate mail function.', + 'invalid_address' => 'Invalid address: ', + 'mailer_not_supported' => ' mailer is not supported.', + 'provide_address' => 'You must provide at least one recipient email address.', + 'recipients_failed' => 'SMTP Error: The following recipients failed: ', + 'signing' => 'Signing Error: ', + 'smtp_connect_failed' => 'SMTP connect() failed.', + 'smtp_error' => 'SMTP server error: ', + 'variable_set' => 'Cannot set or reset variable: ', + 'extension_missing' => 'Extension missing: ', + ]; + if (empty($lang_path)) { + // Calculate an absolute path so it can work if CWD is not here + $lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR; + } + //Validate $langcode + if (!preg_match('/^[a-z]{2}(?:_[a-zA-Z]{2})?$/', $langcode)) { + $langcode = 'en'; + } + $foundlang = true; + $lang_file = $lang_path . 'phpmailer.lang-' . $langcode . '.php'; + // There is no English translation file + if ('en' != $langcode) { + // Make sure language file path is readable + if (!static::isPermittedPath($lang_file) || !file_exists($lang_file)) { + $foundlang = false; + } else { + // Overwrite language-specific strings. + // This way we'll never have missing translation keys. + $foundlang = include $lang_file; + } + } + $this->language = $PHPMAILER_LANG; + + return (bool) $foundlang; // Returns false if language not found + } + + /** + * Get the array of strings for the current language. + * + * @return array + */ + public function getTranslations() + { + return $this->language; + } + + /** + * Create recipient headers. + * + * @param string $type + * @param array $addr An array of recipients, + * where each recipient is a 2-element indexed array with element 0 containing an address + * and element 1 containing a name, like: + * [['joe@example.com', 'Joe User'], ['zoe@example.com', 'Zoe User']] + * + * @return string + */ + public function addrAppend($type, $addr) + { + $addresses = []; + foreach ($addr as $address) { + $addresses[] = $this->addrFormat($address); + } + + return $type . ': ' . implode(', ', $addresses) . static::$LE; + } + + /** + * Format an address for use in a message header. + * + * @param array $addr A 2-element indexed array, element 0 containing an address, element 1 containing a name like + * ['joe@example.com', 'Joe User'] + * + * @return string + */ + public function addrFormat($addr) + { + if (empty($addr[1])) { // No name provided + return $this->secureHeader($addr[0]); + } + + return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') . ' <' . $this->secureHeader( + $addr[0] + ) . '>'; + } + + /** + * Word-wrap message. + * For use with mailers that do not automatically perform wrapping + * and for quoted-printable encoded messages. + * Original written by philippe. + * + * @param string $message The message to wrap + * @param int $length The line length to wrap to + * @param bool $qp_mode Whether to run in Quoted-Printable mode + * + * @return string + */ + public function wrapText($message, $length, $qp_mode = false) + { + if ($qp_mode) { + $soft_break = sprintf(' =%s', static::$LE); + } else { + $soft_break = static::$LE; + } + // If utf-8 encoding is used, we will need to make sure we don't + // split multibyte characters when we wrap + $is_utf8 = static::CHARSET_UTF8 === strtolower($this->CharSet); + $lelen = strlen(static::$LE); + $crlflen = strlen(static::$LE); + + $message = static::normalizeBreaks($message); + //Remove a trailing line break + if (substr($message, -$lelen) == static::$LE) { + $message = substr($message, 0, -$lelen); + } + + //Split message into lines + $lines = explode(static::$LE, $message); + //Message will be rebuilt in here + $message = ''; + foreach ($lines as $line) { + $words = explode(' ', $line); + $buf = ''; + $firstword = true; + foreach ($words as $word) { + if ($qp_mode and (strlen($word) > $length)) { + $space_left = $length - strlen($buf) - $crlflen; + if (!$firstword) { + if ($space_left > 20) { + $len = $space_left; + if ($is_utf8) { + $len = $this->utf8CharBoundary($word, $len); + } elseif ('=' == substr($word, $len - 1, 1)) { + --$len; + } elseif ('=' == substr($word, $len - 2, 1)) { + $len -= 2; + } + $part = substr($word, 0, $len); + $word = substr($word, $len); + $buf .= ' ' . $part; + $message .= $buf . sprintf('=%s', static::$LE); + } else { + $message .= $buf . $soft_break; + } + $buf = ''; + } + while (strlen($word) > 0) { + if ($length <= 0) { + break; + } + $len = $length; + if ($is_utf8) { + $len = $this->utf8CharBoundary($word, $len); + } elseif ('=' == substr($word, $len - 1, 1)) { + --$len; + } elseif ('=' == substr($word, $len - 2, 1)) { + $len -= 2; + } + $part = substr($word, 0, $len); + $word = substr($word, $len); + + if (strlen($word) > 0) { + $message .= $part . sprintf('=%s', static::$LE); + } else { + $buf = $part; + } + } + } else { + $buf_o = $buf; + if (!$firstword) { + $buf .= ' '; + } + $buf .= $word; + + if (strlen($buf) > $length and '' != $buf_o) { + $message .= $buf_o . $soft_break; + $buf = $word; + } + } + $firstword = false; + } + $message .= $buf . static::$LE; + } + + return $message; + } + + /** + * Find the last character boundary prior to $maxLength in a utf-8 + * quoted-printable encoded string. + * Original written by Colin Brown. + * + * @param string $encodedText utf-8 QP text + * @param int $maxLength Find the last character boundary prior to this length + * + * @return int + */ + public function utf8CharBoundary($encodedText, $maxLength) + { + $foundSplitPos = false; + $lookBack = 3; + while (!$foundSplitPos) { + $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack); + $encodedCharPos = strpos($lastChunk, '='); + if (false !== $encodedCharPos) { + // Found start of encoded character byte within $lookBack block. + // Check the encoded byte value (the 2 chars after the '=') + $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2); + $dec = hexdec($hex); + if ($dec < 128) { + // Single byte character. + // If the encoded char was found at pos 0, it will fit + // otherwise reduce maxLength to start of the encoded char + if ($encodedCharPos > 0) { + $maxLength -= $lookBack - $encodedCharPos; + } + $foundSplitPos = true; + } elseif ($dec >= 192) { + // First byte of a multi byte character + // Reduce maxLength to split at start of character + $maxLength -= $lookBack - $encodedCharPos; + $foundSplitPos = true; + } elseif ($dec < 192) { + // Middle byte of a multi byte character, look further back + $lookBack += 3; + } + } else { + // No encoded character found + $foundSplitPos = true; + } + } + + return $maxLength; + } + + /** + * Apply word wrapping to the message body. + * Wraps the message body to the number of chars set in the WordWrap property. + * You should only do this to plain-text bodies as wrapping HTML tags may break them. + * This is called automatically by createBody(), so you don't need to call it yourself. + */ + public function setWordWrap() + { + if ($this->WordWrap < 1) { + return; + } + + switch ($this->message_type) { + case 'alt': + case 'alt_inline': + case 'alt_attach': + case 'alt_inline_attach': + $this->AltBody = $this->wrapText($this->AltBody, $this->WordWrap); + break; + default: + $this->Body = $this->wrapText($this->Body, $this->WordWrap); + break; + } + } + + /** + * Assemble message headers. + * + * @return string The assembled headers + */ + public function createHeader() + { + $result = ''; + + $result .= $this->headerLine('Date', '' == $this->MessageDate ? self::rfcDate() : $this->MessageDate); + + // To be created automatically by mail() + if ($this->SingleTo) { + if ('mail' != $this->Mailer) { + foreach ($this->to as $toaddr) { + $this->SingleToArray[] = $this->addrFormat($toaddr); + } + } + } else { + if (count($this->to) > 0) { + if ('mail' != $this->Mailer) { + $result .= $this->addrAppend('To', $this->to); + } + } elseif (count($this->cc) == 0) { + $result .= $this->headerLine('To', 'undisclosed-recipients:;'); + } + } + + $result .= $this->addrAppend('From', [[trim($this->From), $this->FromName]]); + + // sendmail and mail() extract Cc from the header before sending + if (count($this->cc) > 0) { + $result .= $this->addrAppend('Cc', $this->cc); + } + + // sendmail and mail() extract Bcc from the header before sending + if (( + 'sendmail' == $this->Mailer or 'qmail' == $this->Mailer or 'mail' == $this->Mailer + ) + and count($this->bcc) > 0 + ) { + $result .= $this->addrAppend('Bcc', $this->bcc); + } + + if (count($this->ReplyTo) > 0) { + $result .= $this->addrAppend('Reply-To', $this->ReplyTo); + } + + // mail() sets the subject itself + if ('mail' != $this->Mailer) { + $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject))); + } + + // Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4 + // https://tools.ietf.org/html/rfc5322#section-3.6.4 + if ('' != $this->MessageID and preg_match('/^<.*@.*>$/', $this->MessageID)) { + $this->lastMessageID = $this->MessageID; + } else { + $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname()); + } + $result .= $this->headerLine('Message-ID', $this->lastMessageID); + if (null !== $this->Priority) { + $result .= $this->headerLine('X-Priority', $this->Priority); + } + if ('' == $this->XMailer) { + $result .= $this->headerLine( + 'X-Mailer', + 'PHPMailer ' . self::VERSION . ' (https://github.com/PHPMailer/PHPMailer)' + ); + } else { + $myXmailer = trim($this->XMailer); + if ($myXmailer) { + $result .= $this->headerLine('X-Mailer', $myXmailer); + } + } + + if ('' != $this->ConfirmReadingTo) { + $result .= $this->headerLine('Disposition-Notification-To', '<' . $this->ConfirmReadingTo . '>'); + } + + // Add custom headers + foreach ($this->CustomHeader as $header) { + $result .= $this->headerLine( + trim($header[0]), + $this->encodeHeader(trim($header[1])) + ); + } + if (!$this->sign_key_file) { + $result .= $this->headerLine('MIME-Version', '1.0'); + $result .= $this->getMailMIME(); + } + + return $result; + } + + /** + * Get the message MIME type headers. + * + * @return string + */ + public function getMailMIME() + { + $result = ''; + $ismultipart = true; + switch ($this->message_type) { + case 'inline': + $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';'); + $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"'); + break; + case 'attach': + case 'inline_attach': + case 'alt_attach': + case 'alt_inline_attach': + $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_MIXED . ';'); + $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"'); + break; + case 'alt': + case 'alt_inline': + $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';'); + $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"'); + break; + default: + // Catches case 'plain': and case '': + $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet); + $ismultipart = false; + break; + } + // RFC1341 part 5 says 7bit is assumed if not specified + if (static::ENCODING_7BIT != $this->Encoding) { + // RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE + if ($ismultipart) { + if (static::ENCODING_8BIT == $this->Encoding) { + $result .= $this->headerLine('Content-Transfer-Encoding', static::ENCODING_8BIT); + } + // The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible + } else { + $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding); + } + } + + if ('mail' != $this->Mailer) { + $result .= static::$LE; + } + + return $result; + } + + /** + * Returns the whole MIME message. + * Includes complete headers and body. + * Only valid post preSend(). + * + * @see PHPMailer::preSend() + * + * @return string + */ + public function getSentMIMEMessage() + { + return rtrim($this->MIMEHeader . $this->mailHeader, "\n\r") . static::$LE . static::$LE . $this->MIMEBody; + } + + /** + * Create a unique ID to use for boundaries. + * + * @return string + */ + protected function generateId() + { + $len = 32; //32 bytes = 256 bits + if (function_exists('random_bytes')) { + $bytes = random_bytes($len); + } elseif (function_exists('openssl_random_pseudo_bytes')) { + $bytes = openssl_random_pseudo_bytes($len); + } else { + //Use a hash to force the length to the same as the other methods + $bytes = hash('sha256', uniqid((string) mt_rand(), true), true); + } + + //We don't care about messing up base64 format here, just want a random string + return str_replace(['=', '+', '/'], '', base64_encode(hash('sha256', $bytes, true))); + } + + /** + * Assemble the message body. + * Returns an empty string on failure. + * + * @throws Exception + * + * @return string The assembled message body + */ + public function createBody() + { + $body = ''; + //Create unique IDs and preset boundaries + $this->uniqueid = $this->generateId(); + $this->boundary[1] = 'b1_' . $this->uniqueid; + $this->boundary[2] = 'b2_' . $this->uniqueid; + $this->boundary[3] = 'b3_' . $this->uniqueid; + + if ($this->sign_key_file) { + $body .= $this->getMailMIME() . static::$LE; + } + + $this->setWordWrap(); + + $bodyEncoding = $this->Encoding; + $bodyCharSet = $this->CharSet; + //Can we do a 7-bit downgrade? + if (static::ENCODING_8BIT == $bodyEncoding and !$this->has8bitChars($this->Body)) { + $bodyEncoding = static::ENCODING_7BIT; + //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit + $bodyCharSet = 'us-ascii'; + } + //If lines are too long, and we're not already using an encoding that will shorten them, + //change to quoted-printable transfer encoding for the body part only + if (static::ENCODING_BASE64 != $this->Encoding and static::hasLineLongerThanMax($this->Body)) { + $bodyEncoding = static::ENCODING_QUOTED_PRINTABLE; + } + + $altBodyEncoding = $this->Encoding; + $altBodyCharSet = $this->CharSet; + //Can we do a 7-bit downgrade? + if (static::ENCODING_8BIT == $altBodyEncoding and !$this->has8bitChars($this->AltBody)) { + $altBodyEncoding = static::ENCODING_7BIT; + //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit + $altBodyCharSet = 'us-ascii'; + } + //If lines are too long, and we're not already using an encoding that will shorten them, + //change to quoted-printable transfer encoding for the alt body part only + if (static::ENCODING_BASE64 != $altBodyEncoding and static::hasLineLongerThanMax($this->AltBody)) { + $altBodyEncoding = static::ENCODING_QUOTED_PRINTABLE; + } + //Use this as a preamble in all multipart message types + $mimepre = 'This is a multi-part message in MIME format.' . static::$LE; + switch ($this->message_type) { + case 'inline': + $body .= $mimepre; + $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= static::$LE; + $body .= $this->attachAll('inline', $this->boundary[1]); + break; + case 'attach': + $body .= $mimepre; + $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= static::$LE; + $body .= $this->attachAll('attachment', $this->boundary[1]); + break; + case 'inline_attach': + $body .= $mimepre; + $body .= $this->textLine('--' . $this->boundary[1]); + $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';'); + $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); + $body .= static::$LE; + $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= static::$LE; + $body .= $this->attachAll('inline', $this->boundary[2]); + $body .= static::$LE; + $body .= $this->attachAll('attachment', $this->boundary[1]); + break; + case 'alt': + $body .= $mimepre; + $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, static::CONTENT_TYPE_PLAINTEXT, $altBodyEncoding); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); + $body .= static::$LE; + $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, static::CONTENT_TYPE_TEXT_HTML, $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= static::$LE; + if (!empty($this->Ical)) { + $body .= $this->getBoundary($this->boundary[1], '', static::CONTENT_TYPE_TEXT_CALENDAR . '; method=REQUEST', ''); + $body .= $this->encodeString($this->Ical, $this->Encoding); + $body .= static::$LE; + } + $body .= $this->endBoundary($this->boundary[1]); + break; + case 'alt_inline': + $body .= $mimepre; + $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, static::CONTENT_TYPE_PLAINTEXT, $altBodyEncoding); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); + $body .= static::$LE; + $body .= $this->textLine('--' . $this->boundary[1]); + $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';'); + $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); + $body .= static::$LE; + $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, static::CONTENT_TYPE_TEXT_HTML, $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= static::$LE; + $body .= $this->attachAll('inline', $this->boundary[2]); + $body .= static::$LE; + $body .= $this->endBoundary($this->boundary[1]); + break; + case 'alt_attach': + $body .= $mimepre; + $body .= $this->textLine('--' . $this->boundary[1]); + $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';'); + $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); + $body .= static::$LE; + $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, static::CONTENT_TYPE_PLAINTEXT, $altBodyEncoding); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); + $body .= static::$LE; + $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, static::CONTENT_TYPE_TEXT_HTML, $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= static::$LE; + if (!empty($this->Ical)) { + $body .= $this->getBoundary($this->boundary[2], '', static::CONTENT_TYPE_TEXT_CALENDAR . '; method=REQUEST', ''); + $body .= $this->encodeString($this->Ical, $this->Encoding); + } + $body .= $this->endBoundary($this->boundary[2]); + $body .= static::$LE; + $body .= $this->attachAll('attachment', $this->boundary[1]); + break; + case 'alt_inline_attach': + $body .= $mimepre; + $body .= $this->textLine('--' . $this->boundary[1]); + $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';'); + $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); + $body .= static::$LE; + $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, static::CONTENT_TYPE_PLAINTEXT, $altBodyEncoding); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); + $body .= static::$LE; + $body .= $this->textLine('--' . $this->boundary[2]); + $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';'); + $body .= $this->textLine("\tboundary=\"" . $this->boundary[3] . '"'); + $body .= static::$LE; + $body .= $this->getBoundary($this->boundary[3], $bodyCharSet, static::CONTENT_TYPE_TEXT_HTML, $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= static::$LE; + $body .= $this->attachAll('inline', $this->boundary[3]); + $body .= static::$LE; + $body .= $this->endBoundary($this->boundary[2]); + $body .= static::$LE; + $body .= $this->attachAll('attachment', $this->boundary[1]); + break; + default: + // Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types + //Reset the `Encoding` property in case we changed it for line length reasons + $this->Encoding = $bodyEncoding; + $body .= $this->encodeString($this->Body, $this->Encoding); + break; + } + + if ($this->isError()) { + $body = ''; + if ($this->exceptions) { + throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL); + } + } elseif ($this->sign_key_file) { + try { + if (!defined('PKCS7_TEXT')) { + throw new Exception($this->lang('extension_missing') . 'openssl'); + } + // @TODO would be nice to use php://temp streams here + $file = tempnam(sys_get_temp_dir(), 'mail'); + if (false === file_put_contents($file, $body)) { + throw new Exception($this->lang('signing') . ' Could not write temp file'); + } + $signed = tempnam(sys_get_temp_dir(), 'signed'); + //Workaround for PHP bug https://bugs.php.net/bug.php?id=69197 + if (empty($this->sign_extracerts_file)) { + $sign = @openssl_pkcs7_sign( + $file, + $signed, + 'file://' . realpath($this->sign_cert_file), + ['file://' . realpath($this->sign_key_file), $this->sign_key_pass], + [] + ); + } else { + $sign = @openssl_pkcs7_sign( + $file, + $signed, + 'file://' . realpath($this->sign_cert_file), + ['file://' . realpath($this->sign_key_file), $this->sign_key_pass], + [], + PKCS7_DETACHED, + $this->sign_extracerts_file + ); + } + @unlink($file); + if ($sign) { + $body = file_get_contents($signed); + @unlink($signed); + //The message returned by openssl contains both headers and body, so need to split them up + $parts = explode("\n\n", $body, 2); + $this->MIMEHeader .= $parts[0] . static::$LE . static::$LE; + $body = $parts[1]; + } else { + @unlink($signed); + throw new Exception($this->lang('signing') . openssl_error_string()); + } + } catch (Exception $exc) { + $body = ''; + if ($this->exceptions) { + throw $exc; + } + } + } + + return $body; + } + + /** + * Return the start of a message boundary. + * + * @param string $boundary + * @param string $charSet + * @param string $contentType + * @param string $encoding + * + * @return string + */ + protected function getBoundary($boundary, $charSet, $contentType, $encoding) + { + $result = ''; + if ('' == $charSet) { + $charSet = $this->CharSet; + } + if ('' == $contentType) { + $contentType = $this->ContentType; + } + if ('' == $encoding) { + $encoding = $this->Encoding; + } + $result .= $this->textLine('--' . $boundary); + $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet); + $result .= static::$LE; + // RFC1341 part 5 says 7bit is assumed if not specified + if (static::ENCODING_7BIT != $encoding) { + $result .= $this->headerLine('Content-Transfer-Encoding', $encoding); + } + $result .= static::$LE; + + return $result; + } + + /** + * Return the end of a message boundary. + * + * @param string $boundary + * + * @return string + */ + protected function endBoundary($boundary) + { + return static::$LE . '--' . $boundary . '--' . static::$LE; + } + + /** + * Set the message type. + * PHPMailer only supports some preset message types, not arbitrary MIME structures. + */ + protected function setMessageType() + { + $type = []; + if ($this->alternativeExists()) { + $type[] = 'alt'; + } + if ($this->inlineImageExists()) { + $type[] = 'inline'; + } + if ($this->attachmentExists()) { + $type[] = 'attach'; + } + $this->message_type = implode('_', $type); + if ('' == $this->message_type) { + //The 'plain' message_type refers to the message having a single body element, not that it is plain-text + $this->message_type = 'plain'; + } + } + + /** + * Format a header line. + * + * @param string $name + * @param string|int $value + * + * @return string + */ + public function headerLine($name, $value) + { + return $name . ': ' . $value . static::$LE; + } + + /** + * Return a formatted mail line. + * + * @param string $value + * + * @return string + */ + public function textLine($value) + { + return $value . static::$LE; + } + + /** + * Add an attachment from a path on the filesystem. + * Never use a user-supplied path to a file! + * Returns false if the file could not be found or read. + * Explicitly *does not* support passing URLs; PHPMailer is not an HTTP client. + * If you need to do that, fetch the resource yourself and pass it in via a local file or string. + * + * @param string $path Path to the attachment + * @param string $name Overrides the attachment name + * @param string $encoding File encoding (see $Encoding) + * @param string $type File extension (MIME) type + * @param string $disposition Disposition to use + * + * @throws Exception + * + * @return bool + */ + public function addAttachment($path, $name = '', $encoding = self::ENCODING_BASE64, $type = '', $disposition = 'attachment') + { + try { + if (!static::isPermittedPath($path) || !@is_file($path)) { + throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE); + } + + // If a MIME type is not specified, try to work it out from the file name + if ('' == $type) { + $type = static::filenameToType($path); + } + + $filename = basename($path); + if ('' == $name) { + $name = $filename; + } + + $this->attachment[] = [ + 0 => $path, + 1 => $filename, + 2 => $name, + 3 => $encoding, + 4 => $type, + 5 => false, // isStringAttachment + 6 => $disposition, + 7 => $name, + ]; + } catch (Exception $exc) { + $this->setError($exc->getMessage()); + $this->edebug($exc->getMessage()); + if ($this->exceptions) { + throw $exc; + } + + return false; + } + + return true; + } + + /** + * Return the array of attachments. + * + * @return array + */ + public function getAttachments() + { + return $this->attachment; + } + + /** + * Attach all file, string, and binary attachments to the message. + * Returns an empty string on failure. + * + * @param string $disposition_type + * @param string $boundary + * + * @return string + */ + protected function attachAll($disposition_type, $boundary) + { + // Return text of body + $mime = []; + $cidUniq = []; + $incl = []; + + // Add all attachments + foreach ($this->attachment as $attachment) { + // Check if it is a valid disposition_filter + if ($attachment[6] == $disposition_type) { + // Check for string attachment + $string = ''; + $path = ''; + $bString = $attachment[5]; + if ($bString) { + $string = $attachment[0]; + } else { + $path = $attachment[0]; + } + + $inclhash = hash('sha256', serialize($attachment)); + if (in_array($inclhash, $incl)) { + continue; + } + $incl[] = $inclhash; + $name = $attachment[2]; + $encoding = $attachment[3]; + $type = $attachment[4]; + $disposition = $attachment[6]; + $cid = $attachment[7]; + if ('inline' == $disposition and array_key_exists($cid, $cidUniq)) { + continue; + } + $cidUniq[$cid] = true; + + $mime[] = sprintf('--%s%s', $boundary, static::$LE); + //Only include a filename property if we have one + if (!empty($name)) { + $mime[] = sprintf( + 'Content-Type: %s; name="%s"%s', + $type, + $this->encodeHeader($this->secureHeader($name)), + static::$LE + ); + } else { + $mime[] = sprintf( + 'Content-Type: %s%s', + $type, + static::$LE + ); + } + // RFC1341 part 5 says 7bit is assumed if not specified + if (static::ENCODING_7BIT != $encoding) { + $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, static::$LE); + } + + if (!empty($cid)) { + $mime[] = sprintf('Content-ID: <%s>%s', $cid, static::$LE); + } + + // If a filename contains any of these chars, it should be quoted, + // but not otherwise: RFC2183 & RFC2045 5.1 + // Fixes a warning in IETF's msglint MIME checker + // Allow for bypassing the Content-Disposition header totally + if (!(empty($disposition))) { + $encoded_name = $this->encodeHeader($this->secureHeader($name)); + if (preg_match('/[ \(\)<>@,;:\\"\/\[\]\?=]/', $encoded_name)) { + $mime[] = sprintf( + 'Content-Disposition: %s; filename="%s"%s', + $disposition, + $encoded_name, + static::$LE . static::$LE + ); + } else { + if (!empty($encoded_name)) { + $mime[] = sprintf( + 'Content-Disposition: %s; filename=%s%s', + $disposition, + $encoded_name, + static::$LE . static::$LE + ); + } else { + $mime[] = sprintf( + 'Content-Disposition: %s%s', + $disposition, + static::$LE . static::$LE + ); + } + } + } else { + $mime[] = static::$LE; + } + + // Encode as string attachment + if ($bString) { + $mime[] = $this->encodeString($string, $encoding); + } else { + $mime[] = $this->encodeFile($path, $encoding); + } + if ($this->isError()) { + return ''; + } + $mime[] = static::$LE; + } + } + + $mime[] = sprintf('--%s--%s', $boundary, static::$LE); + + return implode('', $mime); + } + + /** + * Encode a file attachment in requested format. + * Returns an empty string on failure. + * + * @param string $path The full path to the file + * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable' + * + * @throws Exception + * + * @return string + */ + protected function encodeFile($path, $encoding = self::ENCODING_BASE64) + { + try { + if (!static::isPermittedPath($path) || !file_exists($path)) { + throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE); + } + $file_buffer = file_get_contents($path); + if (false === $file_buffer) { + throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE); + } + $file_buffer = $this->encodeString($file_buffer, $encoding); + + return $file_buffer; + } catch (Exception $exc) { + $this->setError($exc->getMessage()); + + return ''; + } + } + + /** + * Encode a string in requested format. + * Returns an empty string on failure. + * + * @param string $str The text to encode + * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable' + * + * @return string + */ + public function encodeString($str, $encoding = self::ENCODING_BASE64) + { + $encoded = ''; + switch (strtolower($encoding)) { + case static::ENCODING_BASE64: + $encoded = chunk_split( + base64_encode($str), + static::STD_LINE_LENGTH, + static::$LE + ); + break; + case static::ENCODING_7BIT: + case static::ENCODING_8BIT: + $encoded = static::normalizeBreaks($str); + // Make sure it ends with a line break + if (substr($encoded, -(strlen(static::$LE))) != static::$LE) { + $encoded .= static::$LE; + } + break; + case static::ENCODING_BINARY: + $encoded = $str; + break; + case static::ENCODING_QUOTED_PRINTABLE: + $encoded = $this->encodeQP($str); + break; + default: + $this->setError($this->lang('encoding') . $encoding); + break; + } + + return $encoded; + } + + /** + * Encode a header value (not including its label) optimally. + * Picks shortest of Q, B, or none. Result includes folding if needed. + * See RFC822 definitions for phrase, comment and text positions. + * + * @param string $str The header value to encode + * @param string $position What context the string will be used in + * + * @return string + */ + public function encodeHeader($str, $position = 'text') + { + $matchcount = 0; + switch (strtolower($position)) { + case 'phrase': + if (!preg_match('/[\200-\377]/', $str)) { + // Can't use addslashes as we don't know the value of magic_quotes_sybase + $encoded = addcslashes($str, "\0..\37\177\\\""); + if (($str == $encoded) and !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) { + return $encoded; + } + + return "\"$encoded\""; + } + $matchcount = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches); + break; + /* @noinspection PhpMissingBreakStatementInspection */ + case 'comment': + $matchcount = preg_match_all('/[()"]/', $str, $matches); + //fallthrough + case 'text': + default: + $matchcount += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches); + break; + } + + //RFCs specify a maximum line length of 78 chars, however mail() will sometimes + //corrupt messages with headers longer than 65 chars. See #818 + $lengthsub = 'mail' == $this->Mailer ? 13 : 0; + $maxlen = static::STD_LINE_LENGTH - $lengthsub; + // Try to select the encoding which should produce the shortest output + if ($matchcount > strlen($str) / 3) { + // More than a third of the content will need encoding, so B encoding will be most efficient + $encoding = 'B'; + //This calculation is: + // max line length + // - shorten to avoid mail() corruption + // - Q/B encoding char overhead ("` =??[QB]??=`") + // - charset name length + $maxlen = static::STD_LINE_LENGTH - $lengthsub - 8 - strlen($this->CharSet); + if ($this->hasMultiBytes($str)) { + // Use a custom function which correctly encodes and wraps long + // multibyte strings without breaking lines within a character + $encoded = $this->base64EncodeWrapMB($str, "\n"); + } else { + $encoded = base64_encode($str); + $maxlen -= $maxlen % 4; + $encoded = trim(chunk_split($encoded, $maxlen, "\n")); + } + $encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded); + } elseif ($matchcount > 0) { + //1 or more chars need encoding, use Q-encode + $encoding = 'Q'; + //Recalc max line length for Q encoding - see comments on B encode + $maxlen = static::STD_LINE_LENGTH - $lengthsub - 8 - strlen($this->CharSet); + $encoded = $this->encodeQ($str, $position); + $encoded = $this->wrapText($encoded, $maxlen, true); + $encoded = str_replace('=' . static::$LE, "\n", trim($encoded)); + $encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded); + } elseif (strlen($str) > $maxlen) { + //No chars need encoding, but line is too long, so fold it + $encoded = trim($this->wrapText($str, $maxlen, false)); + if ($str == $encoded) { + //Wrapping nicely didn't work, wrap hard instead + $encoded = trim(chunk_split($str, static::STD_LINE_LENGTH, static::$LE)); + } + $encoded = str_replace(static::$LE, "\n", trim($encoded)); + $encoded = preg_replace('/^(.*)$/m', ' \\1', $encoded); + } else { + //No reformatting needed + return $str; + } + + return trim(static::normalizeBreaks($encoded)); + } + + /** + * Check if a string contains multi-byte characters. + * + * @param string $str multi-byte text to wrap encode + * + * @return bool + */ + public function hasMultiBytes($str) + { + if (function_exists('mb_strlen')) { + return strlen($str) > mb_strlen($str, $this->CharSet); + } + + // Assume no multibytes (we can't handle without mbstring functions anyway) + return false; + } + + /** + * Does a string contain any 8-bit chars (in any charset)? + * + * @param string $text + * + * @return bool + */ + public function has8bitChars($text) + { + return (bool) preg_match('/[\x80-\xFF]/', $text); + } + + /** + * Encode and wrap long multibyte strings for mail headers + * without breaking lines within a character. + * Adapted from a function by paravoid. + * + * @see http://www.php.net/manual/en/function.mb-encode-mimeheader.php#60283 + * + * @param string $str multi-byte text to wrap encode + * @param string $linebreak string to use as linefeed/end-of-line + * + * @return string + */ + public function base64EncodeWrapMB($str, $linebreak = null) + { + $start = '=?' . $this->CharSet . '?B?'; + $end = '?='; + $encoded = ''; + if (null === $linebreak) { + $linebreak = static::$LE; + } + + $mb_length = mb_strlen($str, $this->CharSet); + // Each line must have length <= 75, including $start and $end + $length = 75 - strlen($start) - strlen($end); + // Average multi-byte ratio + $ratio = $mb_length / strlen($str); + // Base64 has a 4:3 ratio + $avgLength = floor($length * $ratio * .75); + + for ($i = 0; $i < $mb_length; $i += $offset) { + $lookBack = 0; + do { + $offset = $avgLength - $lookBack; + $chunk = mb_substr($str, $i, $offset, $this->CharSet); + $chunk = base64_encode($chunk); + ++$lookBack; + } while (strlen($chunk) > $length); + $encoded .= $chunk . $linebreak; + } + + // Chomp the last linefeed + return substr($encoded, 0, -strlen($linebreak)); + } + + /** + * Encode a string in quoted-printable format. + * According to RFC2045 section 6.7. + * + * @param string $string The text to encode + * + * @return string + */ + public function encodeQP($string) + { + return static::normalizeBreaks(quoted_printable_encode($string)); + } + + /** + * Encode a string using Q encoding. + * + * @see http://tools.ietf.org/html/rfc2047#section-4.2 + * + * @param string $str the text to encode + * @param string $position Where the text is going to be used, see the RFC for what that means + * + * @return string + */ + public function encodeQ($str, $position = 'text') + { + // There should not be any EOL in the string + $pattern = ''; + $encoded = str_replace(["\r", "\n"], '', $str); + switch (strtolower($position)) { + case 'phrase': + // RFC 2047 section 5.3 + $pattern = '^A-Za-z0-9!*+\/ -'; + break; + /* + * RFC 2047 section 5.2. + * Build $pattern without including delimiters and [] + */ + /* @noinspection PhpMissingBreakStatementInspection */ + case 'comment': + $pattern = '\(\)"'; + /* Intentional fall through */ + case 'text': + default: + // RFC 2047 section 5.1 + // Replace every high ascii, control, =, ? and _ characters + /** @noinspection SuspiciousAssignmentsInspection */ + $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $pattern; + break; + } + $matches = []; + if (preg_match_all("/[{$pattern}]/", $encoded, $matches)) { + // If the string contains an '=', make sure it's the first thing we replace + // so as to avoid double-encoding + $eqkey = array_search('=', $matches[0]); + if (false !== $eqkey) { + unset($matches[0][$eqkey]); + array_unshift($matches[0], '='); + } + foreach (array_unique($matches[0]) as $char) { + $encoded = str_replace($char, '=' . sprintf('%02X', ord($char)), $encoded); + } + } + // Replace spaces with _ (more readable than =20) + // RFC 2047 section 4.2(2) + return str_replace(' ', '_', $encoded); + } + + /** + * Add a string or binary attachment (non-filesystem). + * This method can be used to attach ascii or binary data, + * such as a BLOB record from a database. + * + * @param string $string String attachment data + * @param string $filename Name of the attachment + * @param string $encoding File encoding (see $Encoding) + * @param string $type File extension (MIME) type + * @param string $disposition Disposition to use + */ + public function addStringAttachment( + $string, + $filename, + $encoding = self::ENCODING_BASE64, + $type = '', + $disposition = 'attachment' + ) { + // If a MIME type is not specified, try to work it out from the file name + if ('' == $type) { + $type = static::filenameToType($filename); + } + // Append to $attachment array + $this->attachment[] = [ + 0 => $string, + 1 => $filename, + 2 => basename($filename), + 3 => $encoding, + 4 => $type, + 5 => true, // isStringAttachment + 6 => $disposition, + 7 => 0, + ]; + } + + /** + * Add an embedded (inline) attachment from a file. + * This can include images, sounds, and just about any other document type. + * These differ from 'regular' attachments in that they are intended to be + * displayed inline with the message, not just attached for download. + * This is used in HTML messages that embed the images + * the HTML refers to using the $cid value. + * Never use a user-supplied path to a file! + * + * @param string $path Path to the attachment + * @param string $cid Content ID of the attachment; Use this to reference + * the content when using an embedded image in HTML + * @param string $name Overrides the attachment name + * @param string $encoding File encoding (see $Encoding) + * @param string $type File MIME type + * @param string $disposition Disposition to use + * + * @return bool True on successfully adding an attachment + */ + public function addEmbeddedImage($path, $cid, $name = '', $encoding = self::ENCODING_BASE64, $type = '', $disposition = 'inline') + { + if (!static::isPermittedPath($path) || !@is_file($path)) { + $this->setError($this->lang('file_access') . $path); + + return false; + } + + // If a MIME type is not specified, try to work it out from the file name + if ('' == $type) { + $type = static::filenameToType($path); + } + + $filename = basename($path); + if ('' == $name) { + $name = $filename; + } + + // Append to $attachment array + $this->attachment[] = [ + 0 => $path, + 1 => $filename, + 2 => $name, + 3 => $encoding, + 4 => $type, + 5 => false, // isStringAttachment + 6 => $disposition, + 7 => $cid, + ]; + + return true; + } + + /** + * Add an embedded stringified attachment. + * This can include images, sounds, and just about any other document type. + * If your filename doesn't contain an extension, be sure to set the $type to an appropriate MIME type. + * + * @param string $string The attachment binary data + * @param string $cid Content ID of the attachment; Use this to reference + * the content when using an embedded image in HTML + * @param string $name A filename for the attachment. If this contains an extension, + * PHPMailer will attempt to set a MIME type for the attachment. + * For example 'file.jpg' would get an 'image/jpeg' MIME type. + * @param string $encoding File encoding (see $Encoding), defaults to 'base64' + * @param string $type MIME type - will be used in preference to any automatically derived type + * @param string $disposition Disposition to use + * + * @return bool True on successfully adding an attachment + */ + public function addStringEmbeddedImage( + $string, + $cid, + $name = '', + $encoding = self::ENCODING_BASE64, + $type = '', + $disposition = 'inline' + ) { + // If a MIME type is not specified, try to work it out from the name + if ('' == $type and !empty($name)) { + $type = static::filenameToType($name); + } + + // Append to $attachment array + $this->attachment[] = [ + 0 => $string, + 1 => $name, + 2 => $name, + 3 => $encoding, + 4 => $type, + 5 => true, // isStringAttachment + 6 => $disposition, + 7 => $cid, + ]; + + return true; + } + + /** + * Check if an embedded attachment is present with this cid. + * + * @param string $cid + * + * @return bool + */ + protected function cidExists($cid) + { + foreach ($this->attachment as $attachment) { + if ('inline' == $attachment[6] and $cid == $attachment[7]) { + return true; + } + } + + return false; + } + + /** + * Check if an inline attachment is present. + * + * @return bool + */ + public function inlineImageExists() + { + foreach ($this->attachment as $attachment) { + if ('inline' == $attachment[6]) { + return true; + } + } + + return false; + } + + /** + * Check if an attachment (non-inline) is present. + * + * @return bool + */ + public function attachmentExists() + { + foreach ($this->attachment as $attachment) { + if ('attachment' == $attachment[6]) { + return true; + } + } + + return false; + } + + /** + * Check if this message has an alternative body set. + * + * @return bool + */ + public function alternativeExists() + { + return !empty($this->AltBody); + } + + /** + * Clear queued addresses of given kind. + * + * @param string $kind 'to', 'cc', or 'bcc' + */ + public function clearQueuedAddresses($kind) + { + $this->RecipientsQueue = array_filter( + $this->RecipientsQueue, + function ($params) use ($kind) { + return $params[0] != $kind; + } + ); + } + + /** + * Clear all To recipients. + */ + public function clearAddresses() + { + foreach ($this->to as $to) { + unset($this->all_recipients[strtolower($to[0])]); + } + $this->to = []; + $this->clearQueuedAddresses('to'); + } + + /** + * Clear all CC recipients. + */ + public function clearCCs() + { + foreach ($this->cc as $cc) { + unset($this->all_recipients[strtolower($cc[0])]); + } + $this->cc = []; + $this->clearQueuedAddresses('cc'); + } + + /** + * Clear all BCC recipients. + */ + public function clearBCCs() + { + foreach ($this->bcc as $bcc) { + unset($this->all_recipients[strtolower($bcc[0])]); + } + $this->bcc = []; + $this->clearQueuedAddresses('bcc'); + } + + /** + * Clear all ReplyTo recipients. + */ + public function clearReplyTos() + { + $this->ReplyTo = []; + $this->ReplyToQueue = []; + } + + /** + * Clear all recipient types. + */ + public function clearAllRecipients() + { + $this->to = []; + $this->cc = []; + $this->bcc = []; + $this->all_recipients = []; + $this->RecipientsQueue = []; + } + + /** + * Clear all filesystem, string, and binary attachments. + */ + public function clearAttachments() + { + $this->attachment = []; + } + + /** + * Clear all custom headers. + */ + public function clearCustomHeaders() + { + $this->CustomHeader = []; + } + + /** + * Add an error message to the error container. + * + * @param string $msg + */ + protected function setError($msg) + { + ++$this->error_count; + if ('smtp' == $this->Mailer and null !== $this->smtp) { + $lasterror = $this->smtp->getError(); + if (!empty($lasterror['error'])) { + $msg .= $this->lang('smtp_error') . $lasterror['error']; + if (!empty($lasterror['detail'])) { + $msg .= ' Detail: ' . $lasterror['detail']; + } + if (!empty($lasterror['smtp_code'])) { + $msg .= ' SMTP code: ' . $lasterror['smtp_code']; + } + if (!empty($lasterror['smtp_code_ex'])) { + $msg .= ' Additional SMTP info: ' . $lasterror['smtp_code_ex']; + } + } + } + $this->ErrorInfo = $msg; + } + + /** + * Return an RFC 822 formatted date. + * + * @return string + */ + public static function rfcDate() + { + // Set the time zone to whatever the default is to avoid 500 errors + // Will default to UTC if it's not set properly in php.ini + date_default_timezone_set(@date_default_timezone_get()); + + return date('D, j M Y H:i:s O'); + } + + /** + * Get the server hostname. + * Returns 'localhost.localdomain' if unknown. + * + * @return string + */ + protected function serverHostname() + { + $result = ''; + if (!empty($this->Hostname)) { + $result = $this->Hostname; + } elseif (isset($_SERVER) and array_key_exists('SERVER_NAME', $_SERVER)) { + $result = $_SERVER['SERVER_NAME']; + } elseif (function_exists('gethostname') and gethostname() !== false) { + $result = gethostname(); + } elseif (php_uname('n') !== false) { + $result = php_uname('n'); + } + if (!static::isValidHost($result)) { + return 'localhost.localdomain'; + } + + return $result; + } + + /** + * Validate whether a string contains a valid value to use as a hostname or IP address. + * IPv6 addresses must include [], e.g. `[::1]`, not just `::1`. + * + * @param string $host The host name or IP address to check + * + * @return bool + */ + public static function isValidHost($host) + { + //Simple syntax limits + if (empty($host) + or !is_string($host) + or strlen($host) > 256 + ) { + return false; + } + //Looks like a bracketed IPv6 address + if (trim($host, '[]') != $host) { + return (bool) filter_var(trim($host, '[]'), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); + } + //If removing all the dots results in a numeric string, it must be an IPv4 address. + //Need to check this first because otherwise things like `999.0.0.0` are considered valid host names + if (is_numeric(str_replace('.', '', $host))) { + //Is it a valid IPv4 address? + return (bool) filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); + } + if (filter_var('http://' . $host, FILTER_VALIDATE_URL)) { + //Is it a syntactically valid hostname? + return true; + } + + return false; + } + + /** + * Get an error message in the current language. + * + * @param string $key + * + * @return string + */ + protected function lang($key) + { + if (count($this->language) < 1) { + $this->setLanguage('en'); // set the default language + } + + if (array_key_exists($key, $this->language)) { + if ('smtp_connect_failed' == $key) { + //Include a link to troubleshooting docs on SMTP connection failure + //this is by far the biggest cause of support questions + //but it's usually not PHPMailer's fault. + return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting'; + } + + return $this->language[$key]; + } + + //Return the key as a fallback + return $key; + } + + /** + * Check if an error occurred. + * + * @return bool True if an error did occur + */ + public function isError() + { + return $this->error_count > 0; + } + + /** + * Add a custom header. + * $name value can be overloaded to contain + * both header name and value (name:value). + * + * @param string $name Custom header name + * @param string|null $value Header value + */ + public function addCustomHeader($name, $value = null) + { + if (null === $value) { + // Value passed in as name:value + $this->CustomHeader[] = explode(':', $name, 2); + } else { + $this->CustomHeader[] = [$name, $value]; + } + } + + /** + * Returns all custom headers. + * + * @return array + */ + public function getCustomHeaders() + { + return $this->CustomHeader; + } + + /** + * Create a message body from an HTML string. + * Automatically inlines images and creates a plain-text version by converting the HTML, + * overwriting any existing values in Body and AltBody. + * Do not source $message content from user input! + * $basedir is prepended when handling relative URLs, e.g. and must not be empty + * will look for an image file in $basedir/images/a.png and convert it to inline. + * If you don't provide a $basedir, relative paths will be left untouched (and thus probably break in email) + * Converts data-uri images into embedded attachments. + * If you don't want to apply these transformations to your HTML, just set Body and AltBody directly. + * + * @param string $message HTML message string + * @param string $basedir Absolute path to a base directory to prepend to relative paths to images + * @param bool|callable $advanced Whether to use the internal HTML to text converter + * or your own custom converter @see PHPMailer::html2text() + * + * @return string $message The transformed message Body + */ + public function msgHTML($message, $basedir = '', $advanced = false) + { + preg_match_all('/(src|background)=["\'](.*)["\']/Ui', $message, $images); + if (array_key_exists(2, $images)) { + if (strlen($basedir) > 1 && '/' != substr($basedir, -1)) { + // Ensure $basedir has a trailing / + $basedir .= '/'; + } + foreach ($images[2] as $imgindex => $url) { + // Convert data URIs into embedded images + //e.g. "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" + if (preg_match('#^data:(image/(?:jpe?g|gif|png));?(base64)?,(.+)#', $url, $match)) { + if (count($match) == 4 and static::ENCODING_BASE64 == $match[2]) { + $data = base64_decode($match[3]); + } elseif ('' == $match[2]) { + $data = rawurldecode($match[3]); + } else { + //Not recognised so leave it alone + continue; + } + //Hash the decoded data, not the URL so that the same data-URI image used in multiple places + //will only be embedded once, even if it used a different encoding + $cid = hash('sha256', $data) . '@phpmailer.0'; // RFC2392 S 2 + + if (!$this->cidExists($cid)) { + $this->addStringEmbeddedImage($data, $cid, 'embed' . $imgindex, static::ENCODING_BASE64, $match[1]); + } + $message = str_replace( + $images[0][$imgindex], + $images[1][$imgindex] . '="cid:' . $cid . '"', + $message + ); + continue; + } + if (// Only process relative URLs if a basedir is provided (i.e. no absolute local paths) + !empty($basedir) + // Ignore URLs containing parent dir traversal (..) + and (strpos($url, '..') === false) + // Do not change urls that are already inline images + and 0 !== strpos($url, 'cid:') + // Do not change absolute URLs, including anonymous protocol + and !preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url) + ) { + $filename = basename($url); + $directory = dirname($url); + if ('.' == $directory) { + $directory = ''; + } + $cid = hash('sha256', $url) . '@phpmailer.0'; // RFC2392 S 2 + if (strlen($basedir) > 1 and '/' != substr($basedir, -1)) { + $basedir .= '/'; + } + if (strlen($directory) > 1 and '/' != substr($directory, -1)) { + $directory .= '/'; + } + if ($this->addEmbeddedImage( + $basedir . $directory . $filename, + $cid, + $filename, + static::ENCODING_BASE64, + static::_mime_types((string) static::mb_pathinfo($filename, PATHINFO_EXTENSION)) + ) + ) { + $message = preg_replace( + '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui', + $images[1][$imgindex] . '="cid:' . $cid . '"', + $message + ); + } + } + } + } + $this->isHTML(true); + // Convert all message body line breaks to LE, makes quoted-printable encoding work much better + $this->Body = static::normalizeBreaks($message); + $this->AltBody = static::normalizeBreaks($this->html2text($message, $advanced)); + if (!$this->alternativeExists()) { + $this->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.' + . static::$LE; + } + + return $this->Body; + } + + /** + * Convert an HTML string into plain text. + * This is used by msgHTML(). + * Note - older versions of this function used a bundled advanced converter + * which was removed for license reasons in #232. + * Example usage: + * + * ```php + * // Use default conversion + * $plain = $mail->html2text($html); + * // Use your own custom converter + * $plain = $mail->html2text($html, function($html) { + * $converter = new MyHtml2text($html); + * return $converter->get_text(); + * }); + * ``` + * + * @param string $html The HTML text to convert + * @param bool|callable $advanced Any boolean value to use the internal converter, + * or provide your own callable for custom conversion + * + * @return string + */ + public function html2text($html, $advanced = false) + { + if (is_callable($advanced)) { + return call_user_func($advanced, $html); + } + + return html_entity_decode( + trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))), + ENT_QUOTES, + $this->CharSet + ); + } + + /** + * Get the MIME type for a file extension. + * + * @param string $ext File extension + * + * @return string MIME type of file + */ + public static function _mime_types($ext = '') + { + $mimes = [ + 'xl' => 'application/excel', + 'js' => 'application/javascript', + 'hqx' => 'application/mac-binhex40', + 'cpt' => 'application/mac-compactpro', + 'bin' => 'application/macbinary', + 'doc' => 'application/msword', + 'word' => 'application/msword', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12', + 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', + 'class' => 'application/octet-stream', + 'dll' => 'application/octet-stream', + 'dms' => 'application/octet-stream', + 'exe' => 'application/octet-stream', + 'lha' => 'application/octet-stream', + 'lzh' => 'application/octet-stream', + 'psd' => 'application/octet-stream', + 'sea' => 'application/octet-stream', + 'so' => 'application/octet-stream', + 'oda' => 'application/oda', + 'pdf' => 'application/pdf', + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + 'smi' => 'application/smil', + 'smil' => 'application/smil', + 'mif' => 'application/vnd.mif', + 'xls' => 'application/vnd.ms-excel', + 'ppt' => 'application/vnd.ms-powerpoint', + 'wbxml' => 'application/vnd.wap.wbxml', + 'wmlc' => 'application/vnd.wap.wmlc', + 'dcr' => 'application/x-director', + 'dir' => 'application/x-director', + 'dxr' => 'application/x-director', + 'dvi' => 'application/x-dvi', + 'gtar' => 'application/x-gtar', + 'php3' => 'application/x-httpd-php', + 'php4' => 'application/x-httpd-php', + 'php' => 'application/x-httpd-php', + 'phtml' => 'application/x-httpd-php', + 'phps' => 'application/x-httpd-php-source', + 'swf' => 'application/x-shockwave-flash', + 'sit' => 'application/x-stuffit', + 'tar' => 'application/x-tar', + 'tgz' => 'application/x-tar', + 'xht' => 'application/xhtml+xml', + 'xhtml' => 'application/xhtml+xml', + 'zip' => 'application/zip', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mp2' => 'audio/mpeg', + 'mp3' => 'audio/mpeg', + 'm4a' => 'audio/mp4', + 'mpga' => 'audio/mpeg', + 'aif' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'ram' => 'audio/x-pn-realaudio', + 'rm' => 'audio/x-pn-realaudio', + 'rpm' => 'audio/x-pn-realaudio-plugin', + 'ra' => 'audio/x-realaudio', + 'wav' => 'audio/x-wav', + 'mka' => 'audio/x-matroska', + 'bmp' => 'image/bmp', + 'gif' => 'image/gif', + 'jpeg' => 'image/jpeg', + 'jpe' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'png' => 'image/png', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'webp' => 'image/webp', + 'heif' => 'image/heif', + 'heifs' => 'image/heif-sequence', + 'heic' => 'image/heic', + 'heics' => 'image/heic-sequence', + 'eml' => 'message/rfc822', + 'css' => 'text/css', + 'html' => 'text/html', + 'htm' => 'text/html', + 'shtml' => 'text/html', + 'log' => 'text/plain', + 'text' => 'text/plain', + 'txt' => 'text/plain', + 'rtx' => 'text/richtext', + 'rtf' => 'text/rtf', + 'vcf' => 'text/vcard', + 'vcard' => 'text/vcard', + 'ics' => 'text/calendar', + 'xml' => 'text/xml', + 'xsl' => 'text/xml', + 'wmv' => 'video/x-ms-wmv', + 'mpeg' => 'video/mpeg', + 'mpe' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mp4' => 'video/mp4', + 'm4v' => 'video/mp4', + 'mov' => 'video/quicktime', + 'qt' => 'video/quicktime', + 'rv' => 'video/vnd.rn-realvideo', + 'avi' => 'video/x-msvideo', + 'movie' => 'video/x-sgi-movie', + 'webm' => 'video/webm', + 'mkv' => 'video/x-matroska', + ]; + $ext = strtolower($ext); + if (array_key_exists($ext, $mimes)) { + return $mimes[$ext]; + } + + return 'application/octet-stream'; + } + + /** + * Map a file name to a MIME type. + * Defaults to 'application/octet-stream', i.e.. arbitrary binary data. + * + * @param string $filename A file name or full path, does not need to exist as a file + * + * @return string + */ + public static function filenameToType($filename) + { + // In case the path is a URL, strip any query string before getting extension + $qpos = strpos($filename, '?'); + if (false !== $qpos) { + $filename = substr($filename, 0, $qpos); + } + $ext = static::mb_pathinfo($filename, PATHINFO_EXTENSION); + + return static::_mime_types($ext); + } + + /** + * Multi-byte-safe pathinfo replacement. + * Drop-in replacement for pathinfo(), but multibyte- and cross-platform-safe. + * + * @see http://www.php.net/manual/en/function.pathinfo.php#107461 + * + * @param string $path A filename or path, does not need to exist as a file + * @param int|string $options Either a PATHINFO_* constant, + * or a string name to return only the specified piece + * + * @return string|array + */ + public static function mb_pathinfo($path, $options = null) + { + $ret = ['dirname' => '', 'basename' => '', 'extension' => '', 'filename' => '']; + $pathinfo = []; + if (preg_match('#^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^\.\\\\/]+?)|))[\\\\/\.]*$#im', $path, $pathinfo)) { + if (array_key_exists(1, $pathinfo)) { + $ret['dirname'] = $pathinfo[1]; + } + if (array_key_exists(2, $pathinfo)) { + $ret['basename'] = $pathinfo[2]; + } + if (array_key_exists(5, $pathinfo)) { + $ret['extension'] = $pathinfo[5]; + } + if (array_key_exists(3, $pathinfo)) { + $ret['filename'] = $pathinfo[3]; + } + } + switch ($options) { + case PATHINFO_DIRNAME: + case 'dirname': + return $ret['dirname']; + case PATHINFO_BASENAME: + case 'basename': + return $ret['basename']; + case PATHINFO_EXTENSION: + case 'extension': + return $ret['extension']; + case PATHINFO_FILENAME: + case 'filename': + return $ret['filename']; + default: + return $ret; + } + } + + /** + * Set or reset instance properties. + * You should avoid this function - it's more verbose, less efficient, more error-prone and + * harder to debug than setting properties directly. + * Usage Example: + * `$mail->set('SMTPSecure', 'tls');` + * is the same as: + * `$mail->SMTPSecure = 'tls';`. + * + * @param string $name The property name to set + * @param mixed $value The value to set the property to + * + * @return bool + */ + public function set($name, $value = '') + { + if (property_exists($this, $name)) { + $this->$name = $value; + + return true; + } + $this->setError($this->lang('variable_set') . $name); + + return false; + } + + /** + * Strip newlines to prevent header injection. + * + * @param string $str + * + * @return string + */ + public function secureHeader($str) + { + return trim(str_replace(["\r", "\n"], '', $str)); + } + + /** + * Normalize line breaks in a string. + * Converts UNIX LF, Mac CR and Windows CRLF line breaks into a single line break format. + * Defaults to CRLF (for message bodies) and preserves consecutive breaks. + * + * @param string $text + * @param string $breaktype What kind of line break to use; defaults to static::$LE + * + * @return string + */ + public static function normalizeBreaks($text, $breaktype = null) + { + if (null === $breaktype) { + $breaktype = static::$LE; + } + // Normalise to \n + $text = str_replace(["\r\n", "\r"], "\n", $text); + // Now convert LE as needed + if ("\n" !== $breaktype) { + $text = str_replace("\n", $breaktype, $text); + } + + return $text; + } + + /** + * Return the current line break format string. + * + * @return string + */ + public static function getLE() + { + return static::$LE; + } + + /** + * Set the line break format string, e.g. "\r\n". + * + * @param string $le + */ + protected static function setLE($le) + { + static::$LE = $le; + } + + /** + * Set the public and private key files and password for S/MIME signing. + * + * @param string $cert_filename + * @param string $key_filename + * @param string $key_pass Password for private key + * @param string $extracerts_filename Optional path to chain certificate + */ + public function sign($cert_filename, $key_filename, $key_pass, $extracerts_filename = '') + { + $this->sign_cert_file = $cert_filename; + $this->sign_key_file = $key_filename; + $this->sign_key_pass = $key_pass; + $this->sign_extracerts_file = $extracerts_filename; + } + + /** + * Quoted-Printable-encode a DKIM header. + * + * @param string $txt + * + * @return string + */ + public function DKIM_QP($txt) + { + $line = ''; + $len = strlen($txt); + for ($i = 0; $i < $len; ++$i) { + $ord = ord($txt[$i]); + if (((0x21 <= $ord) and ($ord <= 0x3A)) or $ord == 0x3C or ((0x3E <= $ord) and ($ord <= 0x7E))) { + $line .= $txt[$i]; + } else { + $line .= '=' . sprintf('%02X', $ord); + } + } + + return $line; + } + + /** + * Generate a DKIM signature. + * + * @param string $signHeader + * + * @throws Exception + * + * @return string The DKIM signature value + */ + public function DKIM_Sign($signHeader) + { + if (!defined('PKCS7_TEXT')) { + if ($this->exceptions) { + throw new Exception($this->lang('extension_missing') . 'openssl'); + } + + return ''; + } + $privKeyStr = !empty($this->DKIM_private_string) ? + $this->DKIM_private_string : + file_get_contents($this->DKIM_private); + if ('' != $this->DKIM_passphrase) { + $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase); + } else { + $privKey = openssl_pkey_get_private($privKeyStr); + } + if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) { + openssl_pkey_free($privKey); + + return base64_encode($signature); + } + openssl_pkey_free($privKey); + + return ''; + } + + /** + * Generate a DKIM canonicalization header. + * Uses the 'relaxed' algorithm from RFC6376 section 3.4.2. + * Canonicalized headers should *always* use CRLF, regardless of mailer setting. + * + * @see https://tools.ietf.org/html/rfc6376#section-3.4.2 + * + * @param string $signHeader Header + * + * @return string + */ + public function DKIM_HeaderC($signHeader) + { + //Unfold all header continuation lines + //Also collapses folded whitespace. + //Note PCRE \s is too broad a definition of whitespace; RFC5322 defines it as `[ \t]` + //@see https://tools.ietf.org/html/rfc5322#section-2.2 + //That means this may break if you do something daft like put vertical tabs in your headers. + $signHeader = preg_replace('/\r\n[ \t]+/', ' ', $signHeader); + $lines = explode("\r\n", $signHeader); + foreach ($lines as $key => $line) { + //If the header is missing a :, skip it as it's invalid + //This is likely to happen because the explode() above will also split + //on the trailing LE, leaving an empty line + if (strpos($line, ':') === false) { + continue; + } + list($heading, $value) = explode(':', $line, 2); + //Lower-case header name + $heading = strtolower($heading); + //Collapse white space within the value + $value = preg_replace('/[ \t]{2,}/', ' ', $value); + //RFC6376 is slightly unclear here - it says to delete space at the *end* of each value + //But then says to delete space before and after the colon. + //Net result is the same as trimming both ends of the value. + //by elimination, the same applies to the field name + $lines[$key] = trim($heading, " \t") . ':' . trim($value, " \t"); + } + + return implode("\r\n", $lines); + } + + /** + * Generate a DKIM canonicalization body. + * Uses the 'simple' algorithm from RFC6376 section 3.4.3. + * Canonicalized bodies should *always* use CRLF, regardless of mailer setting. + * + * @see https://tools.ietf.org/html/rfc6376#section-3.4.3 + * + * @param string $body Message Body + * + * @return string + */ + public function DKIM_BodyC($body) + { + if (empty($body)) { + return "\r\n"; + } + // Normalize line endings to CRLF + $body = static::normalizeBreaks($body, "\r\n"); + + //Reduce multiple trailing line breaks to a single one + return rtrim($body, "\r\n") . "\r\n"; + } + + /** + * Create the DKIM header and body in a new message header. + * + * @param string $headers_line Header lines + * @param string $subject Subject + * @param string $body Body + * + * @return string + */ + public function DKIM_Add($headers_line, $subject, $body) + { + $DKIMsignatureType = 'rsa-sha256'; // Signature & hash algorithms + $DKIMcanonicalization = 'relaxed/simple'; // Canonicalization of header/body + $DKIMquery = 'dns/txt'; // Query method + $DKIMtime = time(); // Signature Timestamp = seconds since 00:00:00 - Jan 1, 1970 (UTC time zone) + $subject_header = "Subject: $subject"; + $headers = explode(static::$LE, $headers_line); + $from_header = ''; + $to_header = ''; + $date_header = ''; + $current = ''; + $copiedHeaderFields = ''; + $foundExtraHeaders = []; + $extraHeaderKeys = ''; + $extraHeaderValues = ''; + $extraCopyHeaderFields = ''; + foreach ($headers as $header) { + if (strpos($header, 'From:') === 0) { + $from_header = $header; + $current = 'from_header'; + } elseif (strpos($header, 'To:') === 0) { + $to_header = $header; + $current = 'to_header'; + } elseif (strpos($header, 'Date:') === 0) { + $date_header = $header; + $current = 'date_header'; + } elseif (!empty($this->DKIM_extraHeaders)) { + foreach ($this->DKIM_extraHeaders as $extraHeader) { + if (strpos($header, $extraHeader . ':') === 0) { + $headerValue = $header; + foreach ($this->CustomHeader as $customHeader) { + if ($customHeader[0] === $extraHeader) { + $headerValue = trim($customHeader[0]) . + ': ' . + $this->encodeHeader(trim($customHeader[1])); + break; + } + } + $foundExtraHeaders[$extraHeader] = $headerValue; + $current = ''; + break; + } + } + } else { + if (!empty($$current) and strpos($header, ' =?') === 0) { + $$current .= $header; + } else { + $current = ''; + } + } + } + foreach ($foundExtraHeaders as $key => $value) { + $extraHeaderKeys .= ':' . $key; + $extraHeaderValues .= $value . "\r\n"; + if ($this->DKIM_copyHeaderFields) { + $extraCopyHeaderFields .= "\t|" . str_replace('|', '=7C', $this->DKIM_QP($value)) . ";\r\n"; + } + } + if ($this->DKIM_copyHeaderFields) { + $from = str_replace('|', '=7C', $this->DKIM_QP($from_header)); + $to = str_replace('|', '=7C', $this->DKIM_QP($to_header)); + $date = str_replace('|', '=7C', $this->DKIM_QP($date_header)); + $subject = str_replace('|', '=7C', $this->DKIM_QP($subject_header)); + $copiedHeaderFields = "\tz=$from\r\n" . + "\t|$to\r\n" . + "\t|$date\r\n" . + "\t|$subject;\r\n" . + $extraCopyHeaderFields; + } + $body = $this->DKIM_BodyC($body); + $DKIMlen = strlen($body); // Length of body + $DKIMb64 = base64_encode(pack('H*', hash('sha256', $body))); // Base64 of packed binary SHA-256 hash of body + if ('' == $this->DKIM_identity) { + $ident = ''; + } else { + $ident = ' i=' . $this->DKIM_identity . ';'; + } + $dkimhdrs = 'DKIM-Signature: v=1; a=' . + $DKIMsignatureType . '; q=' . + $DKIMquery . '; l=' . + $DKIMlen . '; s=' . + $this->DKIM_selector . + ";\r\n" . + "\tt=" . $DKIMtime . '; c=' . $DKIMcanonicalization . ";\r\n" . + "\th=From:To:Date:Subject" . $extraHeaderKeys . ";\r\n" . + "\td=" . $this->DKIM_domain . ';' . $ident . "\r\n" . + $copiedHeaderFields . + "\tbh=" . $DKIMb64 . ";\r\n" . + "\tb="; + $toSign = $this->DKIM_HeaderC( + $from_header . "\r\n" . + $to_header . "\r\n" . + $date_header . "\r\n" . + $subject_header . "\r\n" . + $extraHeaderValues . + $dkimhdrs + ); + $signed = $this->DKIM_Sign($toSign); + + return static::normalizeBreaks($dkimhdrs . $signed) . static::$LE; + } + + /** + * Detect if a string contains a line longer than the maximum line length + * allowed by RFC 2822 section 2.1.1. + * + * @param string $str + * + * @return bool + */ + public static function hasLineLongerThanMax($str) + { + return (bool) preg_match('/^(.{' . (self::MAX_LINE_LENGTH + strlen(static::$LE)) . ',})/m', $str); + } + + /** + * Allows for public read access to 'to' property. + * Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * + * @return array + */ + public function getToAddresses() + { + return $this->to; + } + + /** + * Allows for public read access to 'cc' property. + * Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * + * @return array + */ + public function getCcAddresses() + { + return $this->cc; + } + + /** + * Allows for public read access to 'bcc' property. + * Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * + * @return array + */ + public function getBccAddresses() + { + return $this->bcc; + } + + /** + * Allows for public read access to 'ReplyTo' property. + * Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * + * @return array + */ + public function getReplyToAddresses() + { + return $this->ReplyTo; + } + + /** + * Allows for public read access to 'all_recipients' property. + * Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * + * @return array + */ + public function getAllRecipientAddresses() + { + return $this->all_recipients; + } + + /** + * Perform a callback. + * + * @param bool $isSent + * @param array $to + * @param array $cc + * @param array $bcc + * @param string $subject + * @param string $body + * @param string $from + * @param array $extra + */ + protected function doCallback($isSent, $to, $cc, $bcc, $subject, $body, $from, $extra) + { + if (!empty($this->action_function) and is_callable($this->action_function)) { + call_user_func($this->action_function, $isSent, $to, $cc, $bcc, $subject, $body, $from, $extra); + } + } + + /** + * Get the OAuth instance. + * + * @return OAuth + */ + public function getOAuth() + { + return $this->oauth; + } + + /** + * Set an OAuth instance. + * + * @param OAuth $oauth + */ + public function setOAuth(OAuth $oauth) + { + $this->oauth = $oauth; + } +} diff --git a/kirby/vendor/phpmailer/phpmailer/src/POP3.php b/kirby/vendor/phpmailer/phpmailer/src/POP3.php new file mode 100755 index 0000000..66cf273 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/src/POP3.php @@ -0,0 +1,419 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2017 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +namespace PHPMailer\PHPMailer; + +/** + * PHPMailer POP-Before-SMTP Authentication Class. + * Specifically for PHPMailer to use for RFC1939 POP-before-SMTP authentication. + * 1) This class does not support APOP authentication. + * 2) Opening and closing lots of POP3 connections can be quite slow. If you need + * to send a batch of emails then just perform the authentication once at the start, + * and then loop through your mail sending script. Providing this process doesn't + * take longer than the verification period lasts on your POP3 server, you should be fine. + * 3) This is really ancient technology; you should only need to use it to talk to very old systems. + * 4) This POP3 class is deliberately lightweight and incomplete, and implements just + * enough to do authentication. + * If you want a more complete class there are other POP3 classes for PHP available. + * + * @author Richard Davey (original author) + * @author Marcus Bointon (Synchro/coolbru) + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + */ +class POP3 +{ + /** + * The POP3 PHPMailer Version number. + * + * @var string + */ + const VERSION = '6.0.7'; + + /** + * Default POP3 port number. + * + * @var int + */ + const DEFAULT_PORT = 110; + + /** + * Default timeout in seconds. + * + * @var int + */ + const DEFAULT_TIMEOUT = 30; + + /** + * Debug display level. + * Options: 0 = no, 1+ = yes. + * + * @var int + */ + public $do_debug = 0; + + /** + * POP3 mail server hostname. + * + * @var string + */ + public $host; + + /** + * POP3 port number. + * + * @var int + */ + public $port; + + /** + * POP3 Timeout Value in seconds. + * + * @var int + */ + public $tval; + + /** + * POP3 username. + * + * @var string + */ + public $username; + + /** + * POP3 password. + * + * @var string + */ + public $password; + + /** + * Resource handle for the POP3 connection socket. + * + * @var resource + */ + protected $pop_conn; + + /** + * Are we connected? + * + * @var bool + */ + protected $connected = false; + + /** + * Error container. + * + * @var array + */ + protected $errors = []; + + /** + * Line break constant. + */ + const LE = "\r\n"; + + /** + * Simple static wrapper for all-in-one POP before SMTP. + * + * @param string $host The hostname to connect to + * @param int|bool $port The port number to connect to + * @param int|bool $timeout The timeout value + * @param string $username + * @param string $password + * @param int $debug_level + * + * @return bool + */ + public static function popBeforeSmtp( + $host, + $port = false, + $timeout = false, + $username = '', + $password = '', + $debug_level = 0 + ) { + $pop = new self(); + + return $pop->authorise($host, $port, $timeout, $username, $password, $debug_level); + } + + /** + * Authenticate with a POP3 server. + * A connect, login, disconnect sequence + * appropriate for POP-before SMTP authorisation. + * + * @param string $host The hostname to connect to + * @param int|bool $port The port number to connect to + * @param int|bool $timeout The timeout value + * @param string $username + * @param string $password + * @param int $debug_level + * + * @return bool + */ + public function authorise($host, $port = false, $timeout = false, $username = '', $password = '', $debug_level = 0) + { + $this->host = $host; + // If no port value provided, use default + if (false === $port) { + $this->port = static::DEFAULT_PORT; + } else { + $this->port = (int) $port; + } + // If no timeout value provided, use default + if (false === $timeout) { + $this->tval = static::DEFAULT_TIMEOUT; + } else { + $this->tval = (int) $timeout; + } + $this->do_debug = $debug_level; + $this->username = $username; + $this->password = $password; + // Reset the error log + $this->errors = []; + // connect + $result = $this->connect($this->host, $this->port, $this->tval); + if ($result) { + $login_result = $this->login($this->username, $this->password); + if ($login_result) { + $this->disconnect(); + + return true; + } + } + // We need to disconnect regardless of whether the login succeeded + $this->disconnect(); + + return false; + } + + /** + * Connect to a POP3 server. + * + * @param string $host + * @param int|bool $port + * @param int $tval + * + * @return bool + */ + public function connect($host, $port = false, $tval = 30) + { + // Are we already connected? + if ($this->connected) { + return true; + } + + //On Windows this will raise a PHP Warning error if the hostname doesn't exist. + //Rather than suppress it with @fsockopen, capture it cleanly instead + set_error_handler([$this, 'catchWarning']); + + if (false === $port) { + $port = static::DEFAULT_PORT; + } + + // connect to the POP3 server + $this->pop_conn = fsockopen( + $host, // POP3 Host + $port, // Port # + $errno, // Error Number + $errstr, // Error Message + $tval + ); // Timeout (seconds) + // Restore the error handler + restore_error_handler(); + + // Did we connect? + if (false === $this->pop_conn) { + // It would appear not... + $this->setError( + "Failed to connect to server $host on port $port. errno: $errno; errstr: $errstr" + ); + + return false; + } + + // Increase the stream time-out + stream_set_timeout($this->pop_conn, $tval, 0); + + // Get the POP3 server response + $pop3_response = $this->getResponse(); + // Check for the +OK + if ($this->checkResponse($pop3_response)) { + // The connection is established and the POP3 server is talking + $this->connected = true; + + return true; + } + + return false; + } + + /** + * Log in to the POP3 server. + * Does not support APOP (RFC 2828, 4949). + * + * @param string $username + * @param string $password + * + * @return bool + */ + public function login($username = '', $password = '') + { + if (!$this->connected) { + $this->setError('Not connected to POP3 server'); + } + if (empty($username)) { + $username = $this->username; + } + if (empty($password)) { + $password = $this->password; + } + + // Send the Username + $this->sendString("USER $username" . static::LE); + $pop3_response = $this->getResponse(); + if ($this->checkResponse($pop3_response)) { + // Send the Password + $this->sendString("PASS $password" . static::LE); + $pop3_response = $this->getResponse(); + if ($this->checkResponse($pop3_response)) { + return true; + } + } + + return false; + } + + /** + * Disconnect from the POP3 server. + */ + public function disconnect() + { + $this->sendString('QUIT'); + //The QUIT command may cause the daemon to exit, which will kill our connection + //So ignore errors here + try { + @fclose($this->pop_conn); + } catch (Exception $e) { + //Do nothing + } + } + + /** + * Get a response from the POP3 server. + * + * @param int $size The maximum number of bytes to retrieve + * + * @return string + */ + protected function getResponse($size = 128) + { + $response = fgets($this->pop_conn, $size); + if ($this->do_debug >= 1) { + echo 'Server -> Client: ', $response; + } + + return $response; + } + + /** + * Send raw data to the POP3 server. + * + * @param string $string + * + * @return int + */ + protected function sendString($string) + { + if ($this->pop_conn) { + if ($this->do_debug >= 2) { //Show client messages when debug >= 2 + echo 'Client -> Server: ', $string; + } + + return fwrite($this->pop_conn, $string, strlen($string)); + } + + return 0; + } + + /** + * Checks the POP3 server response. + * Looks for for +OK or -ERR. + * + * @param string $string + * + * @return bool + */ + protected function checkResponse($string) + { + if (substr($string, 0, 3) !== '+OK') { + $this->setError("Server reported an error: $string"); + + return false; + } + + return true; + } + + /** + * Add an error to the internal error store. + * Also display debug output if it's enabled. + * + * @param string $error + */ + protected function setError($error) + { + $this->errors[] = $error; + if ($this->do_debug >= 1) { + echo '
';
+            foreach ($this->errors as $e) {
+                print_r($e);
+            }
+            echo '
'; + } + } + + /** + * Get an array of error messages, if any. + * + * @return array + */ + public function getErrors() + { + return $this->errors; + } + + /** + * POP3 connection error handler. + * + * @param int $errno + * @param string $errstr + * @param string $errfile + * @param int $errline + */ + protected function catchWarning($errno, $errstr, $errfile, $errline) + { + $this->setError( + 'Connecting to the POP3 server raised a PHP warning:' . + "errno: $errno errstr: $errstr; errfile: $errfile; errline: $errline" + ); + } +} diff --git a/kirby/vendor/phpmailer/phpmailer/src/SMTP.php b/kirby/vendor/phpmailer/phpmailer/src/SMTP.php new file mode 100755 index 0000000..da85442 --- /dev/null +++ b/kirby/vendor/phpmailer/phpmailer/src/SMTP.php @@ -0,0 +1,1326 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2017 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +namespace PHPMailer\PHPMailer; + +/** + * PHPMailer RFC821 SMTP email transport class. + * Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server. + * + * @author Chris Ryan + * @author Marcus Bointon + */ +class SMTP +{ + /** + * The PHPMailer SMTP version number. + * + * @var string + */ + const VERSION = '6.0.7'; + + /** + * SMTP line break constant. + * + * @var string + */ + const LE = "\r\n"; + + /** + * The SMTP port to use if one is not specified. + * + * @var int + */ + const DEFAULT_PORT = 25; + + /** + * The maximum line length allowed by RFC 2822 section 2.1.1. + * + * @var int + */ + const MAX_LINE_LENGTH = 998; + + /** + * Debug level for no output. + */ + const DEBUG_OFF = 0; + + /** + * Debug level to show client -> server messages. + */ + const DEBUG_CLIENT = 1; + + /** + * Debug level to show client -> server and server -> client messages. + */ + const DEBUG_SERVER = 2; + + /** + * Debug level to show connection status, client -> server and server -> client messages. + */ + const DEBUG_CONNECTION = 3; + + /** + * Debug level to show all messages. + */ + const DEBUG_LOWLEVEL = 4; + + /** + * Debug output level. + * Options: + * * self::DEBUG_OFF (`0`) No debug output, default + * * self::DEBUG_CLIENT (`1`) Client commands + * * self::DEBUG_SERVER (`2`) Client commands and server responses + * * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status + * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages. + * + * @var int + */ + public $do_debug = self::DEBUG_OFF; + + /** + * How to handle debug output. + * Options: + * * `echo` Output plain-text as-is, appropriate for CLI + * * `html` Output escaped, line breaks converted to `
`, appropriate for browser output + * * `error_log` Output to error log as configured in php.ini + * Alternatively, you can provide a callable expecting two params: a message string and the debug level: + * + * ```php + * $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";}; + * ``` + * + * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug` + * level output is used: + * + * ```php + * $mail->Debugoutput = new myPsr3Logger; + * ``` + * + * @var string|callable|\Psr\Log\LoggerInterface + */ + public $Debugoutput = 'echo'; + + /** + * Whether to use VERP. + * + * @see http://en.wikipedia.org/wiki/Variable_envelope_return_path + * @see http://www.postfix.org/VERP_README.html Info on VERP + * + * @var bool + */ + public $do_verp = false; + + /** + * The timeout value for connection, in seconds. + * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2. + * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure. + * + * @see http://tools.ietf.org/html/rfc2821#section-4.5.3.2 + * + * @var int + */ + public $Timeout = 300; + + /** + * How long to wait for commands to complete, in seconds. + * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2. + * + * @var int + */ + public $Timelimit = 300; + + /** + * Patterns to extract an SMTP transaction id from reply to a DATA command. + * The first capture group in each regex will be used as the ID. + * MS ESMTP returns the message ID, which may not be correct for internal tracking. + * + * @var string[] + */ + protected $smtp_transaction_id_patterns = [ + 'exim' => '/[\d]{3} OK id=(.*)/', + 'sendmail' => '/[\d]{3} 2.0.0 (.*) Message/', + 'postfix' => '/[\d]{3} 2.0.0 Ok: queued as (.*)/', + 'Microsoft_ESMTP' => '/[0-9]{3} 2.[\d].0 (.*)@(?:.*) Queued mail for delivery/', + 'Amazon_SES' => '/[\d]{3} Ok (.*)/', + 'SendGrid' => '/[\d]{3} Ok: queued as (.*)/', + 'CampaignMonitor' => '/[\d]{3} 2.0.0 OK:([a-zA-Z\d]{48})/', + ]; + + /** + * The last transaction ID issued in response to a DATA command, + * if one was detected. + * + * @var string|bool|null + */ + protected $last_smtp_transaction_id; + + /** + * The socket for the server connection. + * + * @var ?resource + */ + protected $smtp_conn; + + /** + * Error information, if any, for the last SMTP command. + * + * @var array + */ + protected $error = [ + 'error' => '', + 'detail' => '', + 'smtp_code' => '', + 'smtp_code_ex' => '', + ]; + + /** + * The reply the server sent to us for HELO. + * If null, no HELO string has yet been received. + * + * @var string|null + */ + protected $helo_rply = null; + + /** + * The set of SMTP extensions sent in reply to EHLO command. + * Indexes of the array are extension names. + * Value at index 'HELO' or 'EHLO' (according to command that was sent) + * represents the server name. In case of HELO it is the only element of the array. + * Other values can be boolean TRUE or an array containing extension options. + * If null, no HELO/EHLO string has yet been received. + * + * @var array|null + */ + protected $server_caps = null; + + /** + * The most recent reply received from the server. + * + * @var string + */ + protected $last_reply = ''; + + /** + * Output debugging info via a user-selected method. + * + * @param string $str Debug string to output + * @param int $level The debug level of this message; see DEBUG_* constants + * + * @see SMTP::$Debugoutput + * @see SMTP::$do_debug + */ + protected function edebug($str, $level = 0) + { + if ($level > $this->do_debug) { + return; + } + //Is this a PSR-3 logger? + if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) { + $this->Debugoutput->debug($str); + + return; + } + //Avoid clash with built-in function names + if (!in_array($this->Debugoutput, ['error_log', 'html', 'echo']) and is_callable($this->Debugoutput)) { + call_user_func($this->Debugoutput, $str, $level); + + return; + } + switch ($this->Debugoutput) { + case 'error_log': + //Don't output, just log + error_log($str); + break; + case 'html': + //Cleans up output a bit for a better looking, HTML-safe output + echo gmdate('Y-m-d H:i:s'), ' ', htmlentities( + preg_replace('/[\r\n]+/', '', $str), + ENT_QUOTES, + 'UTF-8' + ), "
\n"; + break; + case 'echo': + default: + //Normalize line breaks + $str = preg_replace('/\r\n|\r/ms', "\n", $str); + echo gmdate('Y-m-d H:i:s'), + "\t", + //Trim trailing space + trim( + //Indent for readability, except for trailing break + str_replace( + "\n", + "\n \t ", + trim($str) + ) + ), + "\n"; + } + } + + /** + * Connect to an SMTP server. + * + * @param string $host SMTP server IP or host name + * @param int $port The port number to connect to + * @param int $timeout How long to wait for the connection to open + * @param array $options An array of options for stream_context_create() + * + * @return bool + */ + public function connect($host, $port = null, $timeout = 30, $options = []) + { + static $streamok; + //This is enabled by default since 5.0.0 but some providers disable it + //Check this once and cache the result + if (null === $streamok) { + $streamok = function_exists('stream_socket_client'); + } + // Clear errors to avoid confusion + $this->setError(''); + // Make sure we are __not__ connected + if ($this->connected()) { + // Already connected, generate error + $this->setError('Already connected to a server'); + + return false; + } + if (empty($port)) { + $port = self::DEFAULT_PORT; + } + // Connect to the SMTP server + $this->edebug( + "Connection: opening to $host:$port, timeout=$timeout, options=" . + (count($options) > 0 ? var_export($options, true) : 'array()'), + self::DEBUG_CONNECTION + ); + $errno = 0; + $errstr = ''; + if ($streamok) { + $socket_context = stream_context_create($options); + set_error_handler([$this, 'errorHandler']); + $this->smtp_conn = stream_socket_client( + $host . ':' . $port, + $errno, + $errstr, + $timeout, + STREAM_CLIENT_CONNECT, + $socket_context + ); + restore_error_handler(); + } else { + //Fall back to fsockopen which should work in more places, but is missing some features + $this->edebug( + 'Connection: stream_socket_client not available, falling back to fsockopen', + self::DEBUG_CONNECTION + ); + set_error_handler([$this, 'errorHandler']); + $this->smtp_conn = fsockopen( + $host, + $port, + $errno, + $errstr, + $timeout + ); + restore_error_handler(); + } + // Verify we connected properly + if (!is_resource($this->smtp_conn)) { + $this->setError( + 'Failed to connect to server', + '', + (string) $errno, + (string) $errstr + ); + $this->edebug( + 'SMTP ERROR: ' . $this->error['error'] + . ": $errstr ($errno)", + self::DEBUG_CLIENT + ); + + return false; + } + $this->edebug('Connection: opened', self::DEBUG_CONNECTION); + // SMTP server can take longer to respond, give longer timeout for first read + // Windows does not have support for this timeout function + if (substr(PHP_OS, 0, 3) != 'WIN') { + $max = ini_get('max_execution_time'); + // Don't bother if unlimited + if (0 != $max and $timeout > $max) { + @set_time_limit($timeout); + } + stream_set_timeout($this->smtp_conn, $timeout, 0); + } + // Get any announcement + $announce = $this->get_lines(); + $this->edebug('SERVER -> CLIENT: ' . $announce, self::DEBUG_SERVER); + + return true; + } + + /** + * Initiate a TLS (encrypted) session. + * + * @return bool + */ + public function startTLS() + { + if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) { + return false; + } + + //Allow the best TLS version(s) we can + $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT; + + //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT + //so add them back in manually if we can + if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) { + $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; + $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; + } + + // Begin encrypted connection + set_error_handler([$this, 'errorHandler']); + $crypto_ok = stream_socket_enable_crypto( + $this->smtp_conn, + true, + $crypto_method + ); + restore_error_handler(); + + return (bool) $crypto_ok; + } + + /** + * Perform SMTP authentication. + * Must be run after hello(). + * + * @see hello() + * + * @param string $username The user name + * @param string $password The password + * @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2) + * @param OAuth $OAuth An optional OAuth instance for XOAUTH2 authentication + * + * @return bool True if successfully authenticated + */ + public function authenticate( + $username, + $password, + $authtype = null, + $OAuth = null + ) { + if (!$this->server_caps) { + $this->setError('Authentication is not allowed before HELO/EHLO'); + + return false; + } + + if (array_key_exists('EHLO', $this->server_caps)) { + // SMTP extensions are available; try to find a proper authentication method + if (!array_key_exists('AUTH', $this->server_caps)) { + $this->setError('Authentication is not allowed at this stage'); + // 'at this stage' means that auth may be allowed after the stage changes + // e.g. after STARTTLS + + return false; + } + + $this->edebug('Auth method requested: ' . ($authtype ? $authtype : 'UNSPECIFIED'), self::DEBUG_LOWLEVEL); + $this->edebug( + 'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']), + self::DEBUG_LOWLEVEL + ); + + //If we have requested a specific auth type, check the server supports it before trying others + if (null !== $authtype and !in_array($authtype, $this->server_caps['AUTH'])) { + $this->edebug('Requested auth method not available: ' . $authtype, self::DEBUG_LOWLEVEL); + $authtype = null; + } + + if (empty($authtype)) { + //If no auth mechanism is specified, attempt to use these, in this order + //Try CRAM-MD5 first as it's more secure than the others + foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'] as $method) { + if (in_array($method, $this->server_caps['AUTH'])) { + $authtype = $method; + break; + } + } + if (empty($authtype)) { + $this->setError('No supported authentication methods found'); + + return false; + } + self::edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL); + } + + if (!in_array($authtype, $this->server_caps['AUTH'])) { + $this->setError("The requested authentication method \"$authtype\" is not supported by the server"); + + return false; + } + } elseif (empty($authtype)) { + $authtype = 'LOGIN'; + } + switch ($authtype) { + case 'PLAIN': + // Start authentication + if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) { + return false; + } + // Send encoded username and password + if (!$this->sendCommand( + 'User & Password', + base64_encode("\0" . $username . "\0" . $password), + 235 + ) + ) { + return false; + } + break; + case 'LOGIN': + // Start authentication + if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) { + return false; + } + if (!$this->sendCommand('Username', base64_encode($username), 334)) { + return false; + } + if (!$this->sendCommand('Password', base64_encode($password), 235)) { + return false; + } + break; + case 'CRAM-MD5': + // Start authentication + if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) { + return false; + } + // Get the challenge + $challenge = base64_decode(substr($this->last_reply, 4)); + + // Build the response + $response = $username . ' ' . $this->hmac($challenge, $password); + + // send encoded credentials + return $this->sendCommand('Username', base64_encode($response), 235); + case 'XOAUTH2': + //The OAuth instance must be set up prior to requesting auth. + if (null === $OAuth) { + return false; + } + $oauth = $OAuth->getOauth64(); + + // Start authentication + if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) { + return false; + } + break; + default: + $this->setError("Authentication method \"$authtype\" is not supported"); + + return false; + } + + return true; + } + + /** + * Calculate an MD5 HMAC hash. + * Works like hash_hmac('md5', $data, $key) + * in case that function is not available. + * + * @param string $data The data to hash + * @param string $key The key to hash with + * + * @return string + */ + protected function hmac($data, $key) + { + if (function_exists('hash_hmac')) { + return hash_hmac('md5', $data, $key); + } + + // The following borrowed from + // http://php.net/manual/en/function.mhash.php#27225 + + // RFC 2104 HMAC implementation for php. + // Creates an md5 HMAC. + // Eliminates the need to install mhash to compute a HMAC + // by Lance Rushing + + $bytelen = 64; // byte length for md5 + if (strlen($key) > $bytelen) { + $key = pack('H*', md5($key)); + } + $key = str_pad($key, $bytelen, chr(0x00)); + $ipad = str_pad('', $bytelen, chr(0x36)); + $opad = str_pad('', $bytelen, chr(0x5c)); + $k_ipad = $key ^ $ipad; + $k_opad = $key ^ $opad; + + return md5($k_opad . pack('H*', md5($k_ipad . $data))); + } + + /** + * Check connection state. + * + * @return bool True if connected + */ + public function connected() + { + if (is_resource($this->smtp_conn)) { + $sock_status = stream_get_meta_data($this->smtp_conn); + if ($sock_status['eof']) { + // The socket is valid but we are not connected + $this->edebug( + 'SMTP NOTICE: EOF caught while checking if connected', + self::DEBUG_CLIENT + ); + $this->close(); + + return false; + } + + return true; // everything looks good + } + + return false; + } + + /** + * Close the socket and clean up the state of the class. + * Don't use this function without first trying to use QUIT. + * + * @see quit() + */ + public function close() + { + $this->setError(''); + $this->server_caps = null; + $this->helo_rply = null; + if (is_resource($this->smtp_conn)) { + // close the connection and cleanup + fclose($this->smtp_conn); + $this->smtp_conn = null; //Makes for cleaner serialization + $this->edebug('Connection: closed', self::DEBUG_CONNECTION); + } + } + + /** + * Send an SMTP DATA command. + * Issues a data command and sends the msg_data to the server, + * finializing the mail transaction. $msg_data is the message + * that is to be send with the headers. Each header needs to be + * on a single line followed by a with the message headers + * and the message body being separated by an additional . + * Implements RFC 821: DATA . + * + * @param string $msg_data Message data to send + * + * @return bool + */ + public function data($msg_data) + { + //This will use the standard timelimit + if (!$this->sendCommand('DATA', 'DATA', 354)) { + return false; + } + + /* The server is ready to accept data! + * According to rfc821 we should not send more than 1000 characters on a single line (including the LE) + * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into + * smaller lines to fit within the limit. + * We will also look for lines that start with a '.' and prepend an additional '.'. + * NOTE: this does not count towards line-length limit. + */ + + // Normalize line breaks before exploding + $lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $msg_data)); + + /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field + * of the first line (':' separated) does not contain a space then it _should_ be a header and we will + * process all lines before a blank line as headers. + */ + + $field = substr($lines[0], 0, strpos($lines[0], ':')); + $in_headers = false; + if (!empty($field) and strpos($field, ' ') === false) { + $in_headers = true; + } + + foreach ($lines as $line) { + $lines_out = []; + if ($in_headers and $line == '') { + $in_headers = false; + } + //Break this line up into several smaller lines if it's too long + //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len), + while (isset($line[self::MAX_LINE_LENGTH])) { + //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on + //so as to avoid breaking in the middle of a word + $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' '); + //Deliberately matches both false and 0 + if (!$pos) { + //No nice break found, add a hard break + $pos = self::MAX_LINE_LENGTH - 1; + $lines_out[] = substr($line, 0, $pos); + $line = substr($line, $pos); + } else { + //Break at the found point + $lines_out[] = substr($line, 0, $pos); + //Move along by the amount we dealt with + $line = substr($line, $pos + 1); + } + //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1 + if ($in_headers) { + $line = "\t" . $line; + } + } + $lines_out[] = $line; + + //Send the lines to the server + foreach ($lines_out as $line_out) { + //RFC2821 section 4.5.2 + if (!empty($line_out) and $line_out[0] == '.') { + $line_out = '.' . $line_out; + } + $this->client_send($line_out . static::LE, 'DATA'); + } + } + + //Message data has been sent, complete the command + //Increase timelimit for end of DATA command + $savetimelimit = $this->Timelimit; + $this->Timelimit = $this->Timelimit * 2; + $result = $this->sendCommand('DATA END', '.', 250); + $this->recordLastTransactionID(); + //Restore timelimit + $this->Timelimit = $savetimelimit; + + return $result; + } + + /** + * Send an SMTP HELO or EHLO command. + * Used to identify the sending server to the receiving server. + * This makes sure that client and server are in a known state. + * Implements RFC 821: HELO + * and RFC 2821 EHLO. + * + * @param string $host The host name or IP to connect to + * + * @return bool + */ + public function hello($host = '') + { + //Try extended hello first (RFC 2821) + return $this->sendHello('EHLO', $host) or $this->sendHello('HELO', $host); + } + + /** + * Send an SMTP HELO or EHLO command. + * Low-level implementation used by hello(). + * + * @param string $hello The HELO string + * @param string $host The hostname to say we are + * + * @return bool + * + * @see hello() + */ + protected function sendHello($hello, $host) + { + $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250); + $this->helo_rply = $this->last_reply; + if ($noerror) { + $this->parseHelloFields($hello); + } else { + $this->server_caps = null; + } + + return $noerror; + } + + /** + * Parse a reply to HELO/EHLO command to discover server extensions. + * In case of HELO, the only parameter that can be discovered is a server name. + * + * @param string $type `HELO` or `EHLO` + */ + protected function parseHelloFields($type) + { + $this->server_caps = []; + $lines = explode("\n", $this->helo_rply); + + foreach ($lines as $n => $s) { + //First 4 chars contain response code followed by - or space + $s = trim(substr($s, 4)); + if (empty($s)) { + continue; + } + $fields = explode(' ', $s); + if (!empty($fields)) { + if (!$n) { + $name = $type; + $fields = $fields[0]; + } else { + $name = array_shift($fields); + switch ($name) { + case 'SIZE': + $fields = ($fields ? $fields[0] : 0); + break; + case 'AUTH': + if (!is_array($fields)) { + $fields = []; + } + break; + default: + $fields = true; + } + } + $this->server_caps[$name] = $fields; + } + } + } + + /** + * Send an SMTP MAIL command. + * Starts a mail transaction from the email address specified in + * $from. Returns true if successful or false otherwise. If True + * the mail transaction is started and then one or more recipient + * commands may be called followed by a data command. + * Implements RFC 821: MAIL FROM: . + * + * @param string $from Source address of this message + * + * @return bool + */ + public function mail($from) + { + $useVerp = ($this->do_verp ? ' XVERP' : ''); + + return $this->sendCommand( + 'MAIL FROM', + 'MAIL FROM:<' . $from . '>' . $useVerp, + 250 + ); + } + + /** + * Send an SMTP QUIT command. + * Closes the socket if there is no error or the $close_on_error argument is true. + * Implements from RFC 821: QUIT . + * + * @param bool $close_on_error Should the connection close if an error occurs? + * + * @return bool + */ + public function quit($close_on_error = true) + { + $noerror = $this->sendCommand('QUIT', 'QUIT', 221); + $err = $this->error; //Save any error + if ($noerror or $close_on_error) { + $this->close(); + $this->error = $err; //Restore any error from the quit command + } + + return $noerror; + } + + /** + * Send an SMTP RCPT command. + * Sets the TO argument to $toaddr. + * Returns true if the recipient was accepted false if it was rejected. + * Implements from RFC 821: RCPT TO: . + * + * @param string $address The address the message is being sent to + * + * @return bool + */ + public function recipient($address) + { + return $this->sendCommand( + 'RCPT TO', + 'RCPT TO:<' . $address . '>', + [250, 251] + ); + } + + /** + * Send an SMTP RSET command. + * Abort any transaction that is currently in progress. + * Implements RFC 821: RSET . + * + * @return bool True on success + */ + public function reset() + { + return $this->sendCommand('RSET', 'RSET', 250); + } + + /** + * Send a command to an SMTP server and check its return code. + * + * @param string $command The command name - not sent to the server + * @param string $commandstring The actual command to send + * @param int|array $expect One or more expected integer success codes + * + * @return bool True on success + */ + protected function sendCommand($command, $commandstring, $expect) + { + if (!$this->connected()) { + $this->setError("Called $command without being connected"); + + return false; + } + //Reject line breaks in all commands + if (strpos($commandstring, "\n") !== false or strpos($commandstring, "\r") !== false) { + $this->setError("Command '$command' contained line breaks"); + + return false; + } + $this->client_send($commandstring . static::LE, $command); + + $this->last_reply = $this->get_lines(); + // Fetch SMTP code and possible error code explanation + $matches = []; + if (preg_match('/^([0-9]{3})[ -](?:([0-9]\\.[0-9]\\.[0-9]) )?/', $this->last_reply, $matches)) { + $code = $matches[1]; + $code_ex = (count($matches) > 2 ? $matches[2] : null); + // Cut off error code from each response line + $detail = preg_replace( + "/{$code}[ -]" . + ($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . '/m', + '', + $this->last_reply + ); + } else { + // Fall back to simple parsing if regex fails + $code = substr($this->last_reply, 0, 3); + $code_ex = null; + $detail = substr($this->last_reply, 4); + } + + $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER); + + if (!in_array($code, (array) $expect)) { + $this->setError( + "$command command failed", + $detail, + $code, + $code_ex + ); + $this->edebug( + 'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply, + self::DEBUG_CLIENT + ); + + return false; + } + + $this->setError(''); + + return true; + } + + /** + * Send an SMTP SAML command. + * Starts a mail transaction from the email address specified in $from. + * Returns true if successful or false otherwise. If True + * the mail transaction is started and then one or more recipient + * commands may be called followed by a data command. This command + * will send the message to the users terminal if they are logged + * in and send them an email. + * Implements RFC 821: SAML FROM: . + * + * @param string $from The address the message is from + * + * @return bool + */ + public function sendAndMail($from) + { + return $this->sendCommand('SAML', "SAML FROM:$from", 250); + } + + /** + * Send an SMTP VRFY command. + * + * @param string $name The name to verify + * + * @return bool + */ + public function verify($name) + { + return $this->sendCommand('VRFY', "VRFY $name", [250, 251]); + } + + /** + * Send an SMTP NOOP command. + * Used to keep keep-alives alive, doesn't actually do anything. + * + * @return bool + */ + public function noop() + { + return $this->sendCommand('NOOP', 'NOOP', 250); + } + + /** + * Send an SMTP TURN command. + * This is an optional command for SMTP that this class does not support. + * This method is here to make the RFC821 Definition complete for this class + * and _may_ be implemented in future. + * Implements from RFC 821: TURN . + * + * @return bool + */ + public function turn() + { + $this->setError('The SMTP TURN command is not implemented'); + $this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT); + + return false; + } + + /** + * Send raw data to the server. + * + * @param string $data The data to send + * @param string $command Optionally, the command this is part of, used only for controlling debug output + * + * @return int|bool The number of bytes sent to the server or false on error + */ + public function client_send($data, $command = '') + { + //If SMTP transcripts are left enabled, or debug output is posted online + //it can leak credentials, so hide credentials in all but lowest level + if (self::DEBUG_LOWLEVEL > $this->do_debug and + in_array($command, ['User & Password', 'Username', 'Password'], true)) { + $this->edebug('CLIENT -> SERVER: