commit 01277f79f23f396cf92faf4ebb1d1f9a042300e3 Author: Bastian Allgeier Date: Sun Jan 13 23:17:34 2019 +0100 first version diff --git a/.editorconfig b/.editorconfig new file mode 100644 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 100644 index 0000000..27bde24 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# System files +# ------------ + +Icon +.DS_Store + +# Temporary files +# --------------- + +/media/* +!/media/index.html + +# -------------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..c667780 --- /dev/null +++ b/.htaccess @@ -0,0 +1,57 @@ +# 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 enviroments 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 + diff --git a/README.md b/README.md new file mode 100644 index 0000000..060cf72 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# Kirby Starterkit + +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 Starterkit + +Kirby's Starterkit comes with a small demo website and a fully +configured panel. Feel free to modify it and play with it as +much as you like. + +There's also the [Langkit](https://github.com/getkirby/langkit.git) +in case you need a multi-language installation. + +## 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 Starterkit +from https://download.getkirby.com + +### With Git + +If you are familiar with Git, you can clone Kirby's +Starterkit repository from Github. + + git clone https://github.com/getkirby/starterkit.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/assets/css/index.css b/assets/css/index.css new file mode 100644 index 0000000..7bed948 --- /dev/null +++ b/assets/css/index.css @@ -0,0 +1,176 @@ +:root { + --content-width: 65rem; +} + +*, +*:after, +*:before{ + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";; +} + +li { + list-style: none; +} + +a { + color: currentColor; + text-decoration: none; +} + +strong, b { + font-weight: 500; +} + +img { + width: 100%; +} + +.page { + padding: 5vh 5vw 10vh; +} +.page > * { + max-width: var(--content-width); + margin: 0 auto; +} + +.header { + margin-bottom: 1.5rem; +} + +.header a { + position: relative; + text-transform: uppercase; + font-size: .875rem; + letter-spacing: .05em; + padding: .5rem 0; + font-weight: 700; +} + +.header .logo { + display: block; + margin-bottom: 1.5rem; + padding: .5rem 0; +} + +.header { + display: flex; + flex-direction: column; + align-items: center; +} + +.menu a { + margin: 0 .75rem; +} + +.menu a[aria-current] { + border-bottom: 2px solid #000; +} + +@media screen and (min-width: 40rem) { + .header .logo { + margin-bottom: 0; + } + .header { + flex-direction: row; + justify-content: space-between; + } + .menu { + margin-right: -.75rem; + } +} + + +main { + min-height: calc(100vh - 10rem); +} + +.intro { + padding: 10vh 0; + text-align: center; +} + +.intro h1 { + position: relative; + margin-bottom: 1rem; + font-weight: 900; + font-size: calc(1vw + 2rem); + z-index: 1; +} + +.tags { + text-align: center; + text-transform: uppercase; + letter-spacing: 0.075em; + font-size: .75rem; + font-weight: 600; +} + +.text { + line-height: 1.5em; +} +.text p, +.text figure, +.text ul, +.text ol { + margin-bottom: 1.5em; +} +.text h2 { + font-size: 1.5rem; + font-weight: 700; + margin-top: 3rem; + margin-bottom: 1.5rem; + text-align: center; +} +.text > *:first-child { + margin-top: 0; +} +.text a { + position: relative; + white-space: nowrap; + font-weight: 500; + z-index: 1; + display: inline-block; + border-bottom: 2px solid #000; +} +.text figure { + padding-top: 1.5rem; + padding-bottom: 1.5rem; +} +.text img { + width: 100%; +} + +.footer { + padding: 1.5rem 5vw 10vh; + text-align: center; + max-width: var(--content-width); + margin: 0 auto; + line-height: 1.5em; +} +.footer a { + display: inline-block; + font-size: .875rem; +} +.footer > a { + margin-bottom: 1.5rem; + border-top: 2px solid #000; + width: 16.5rem; + padding-top: .5rem; +} + +.social a { + margin: 0 .75rem; + padding: .5rem 1rem; + border: 2px solid #000; + width: 7.5rem; +} +.social a:hover { + background: #000; + color: #fff; +} diff --git a/assets/css/templates/about.css b/assets/css/templates/about.css new file mode 100644 index 0000000..ea15bad --- /dev/null +++ b/assets/css/templates/about.css @@ -0,0 +1,19 @@ +.layout { + display: grid; + grid-template-columns: 1fr; + grid-gap: 3rem; +} + +@media screen and (min-width: 45rem) { + .layout { + grid-template-columns: 1fr 2fr; + } +} + +.layout aside section { + margin-bottom: 3rem; +} + +.layout aside h2 { + margin-bottom: .75rem; +} diff --git a/assets/css/templates/album.css b/assets/css/templates/album.css new file mode 100644 index 0000000..8e55ca2 --- /dev/null +++ b/assets/css/templates/album.css @@ -0,0 +1,57 @@ +.album-cover { + position: relative; + line-height: 0; + margin-bottom: 6rem; + background: #000; + padding-bottom: 75%; +} +.album-cover figcaption { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + bottom: 0; + left: 0; + right: 0; + top: 0; + background: rgba(0,0,0, .5); + text-align: center; + color: #fff; + line-height: 1; + padding: 1.5rem; +} +.album-cover img { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; +} +.album-cover h1 { + font-size: 3rem; +} +.album-text { + max-width: 40rem; + margin: 0 auto 6rem; + text-align: center; +} +.album-gallery { + display: grid; + grid-template-columns: repeat(3, 1fr); + align-items: center; + margin: 0 auto; + grid-gap: 1rem; + max-width: calc(var(--content-width) - 15rem); + justify-content: center; +} +.album-gallery[data-even] { + grid-template-columns: repeat(4, 1fr); +} +.album-gallery[data-count="1"] { + grid-template-columns: 1fr; +} +.album-gallery[data-count="2"] { + grid-template-columns: 1fr 1fr; +} diff --git a/assets/css/templates/default.css b/assets/css/templates/default.css new file mode 100644 index 0000000..5929d9c --- /dev/null +++ b/assets/css/templates/default.css @@ -0,0 +1,4 @@ +.text { + max-width: 35rem; + margin: 0 auto; +} diff --git a/assets/css/templates/home.css b/assets/css/templates/home.css new file mode 100644 index 0000000..861b9d8 --- /dev/null +++ b/assets/css/templates/home.css @@ -0,0 +1,76 @@ +.grid { + display: grid; + list-style: none; + grid-gap: 1rem; + line-height: 0; + grid-template-columns: repeat(1, 1fr); + grid-auto-flow: dense; +} +.grid li { + position: relative; + --cols: 1; + --rows: 1; + + overflow: hidden; + background: #000; + line-height: 0; +} +.grid li:first-child { + --cols: 2; + --rows: 2; +} +.grid li:nth-child(5) { + --cols: 2; +} +.grid li:nth-child(6) { + --rows: 2; +} +.grid li:nth-child(7) { + --cols: 2; +} +.grid a { + display: block; + height: 10rem; +} +.grid img { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + transition: all .3s; +} +.grid figcaption { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + color: #fff; + top: 0; + right: 0; + bottom: 0; + left: 0; + line-height: 1; + text-align: center; + background: rgba(0,0,0, .5); + text-transform: uppercase; + font-size: .875rem; + letter-spacing: .125em; + font-weight: 600; +} + +@media screen and (min-width: 45em) { + .grid { + grid-template-columns: repeat(3, 1fr); + } + .grid li { + grid-column-start: span var(--cols); + grid-row-start: span var(--rows); + } + .grid a { + padding-bottom: 52.65%; + } +} diff --git a/assets/css/templates/note.css b/assets/css/templates/note.css new file mode 100644 index 0000000..fc33691 --- /dev/null +++ b/assets/css/templates/note.css @@ -0,0 +1,33 @@ +.note { + max-width: 35rem; + margin: 0 auto; +} +.note-header { + text-align: center; +} +.note-date { + margin-bottom: .5rem; + display: block; +} + +.gallery { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); + grid-gap: 1.5rem; + margin-bottom: 1.5rem; + padding: 3rem 0; +} +.gallery figure a { + border: 0; +} +.gallery figure { + margin: 0; + padding: 0; +} + +@media screen and (min-width: 45rem) { + .gallery { + margin-left: -3rem; + margin-right: -3rem; + } +} diff --git a/assets/css/templates/notes.css b/assets/css/templates/notes.css new file mode 100644 index 0000000..b9d78b2 --- /dev/null +++ b/assets/css/templates/notes.css @@ -0,0 +1,20 @@ +.notes { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); + grid-gap: 1.5rem; + grid-auto-rows: 1fr; +} +.note { + border: 2px solid #000; +} +.note a { + display: block; + padding: 1rem; + line-height: 1.25em; +} +.note h2 { + font-size: 1rem; +} +.note time { + font-size: .75rem; +} diff --git a/assets/css/templates/photography.css b/assets/css/templates/photography.css new file mode 100644 index 0000000..5b8ae49 --- /dev/null +++ b/assets/css/templates/photography.css @@ -0,0 +1,62 @@ +.albums { + display: grid; + list-style: none; + grid-gap: 1rem; + line-height: 0; +} + +@media screen and (min-width: 30em) { + .albums { + grid-template-columns: repeat(2, 1fr); + } +} +@media screen and (min-width: 60em) { + .albums { + grid-template-columns: repeat(3, 1fr); + } + .albums[data-even] { + grid-template-columns: repeat(4, 1fr); + } +} + + +.albums li { + overflow: hidden; + background: #000; +} +.albums figure { + position: relative; + padding-bottom: 125%; +} +.albums figcaption { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + color: #fff; + background: rgba(0,0,0, .5); + display: flex; + justify-content: space-between; + align-items: flex-end; + line-height: 1.5em; + padding: 1rem; + font-size: .875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: .125em; +} +.albums img { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + transition: all .3s; +} +.albums img:hover { + opacity: .2; +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7595edc --- /dev/null +++ b/composer.json @@ -0,0 +1,27 @@ +{ + "name": "getkirby/starterkit", + "description": "Kirby Starterkit", + "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/1_photography/1_animals/abba.jpg b/content/1_photography/1_animals/abba.jpg new file mode 100644 index 0000000..623b825 Binary files /dev/null and b/content/1_photography/1_animals/abba.jpg differ diff --git a/content/1_photography/1_animals/abba.jpg.txt b/content/1_photography/1_animals/abba.jpg.txt new file mode 100644 index 0000000..3f1a9e8 --- /dev/null +++ b/content/1_photography/1_animals/abba.jpg.txt @@ -0,0 +1,25 @@ +Caption: + +---- + +Alt: A colorful fish + +---- + +Photographer: Pietro Jeng + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/0Sd2qqU5soQ + +---- + +Template: image + +---- + +Sort: 4 \ No newline at end of file diff --git a/content/1_photography/1_animals/album.txt b/content/1_photography/1_animals/album.txt new file mode 100644 index 0000000..7d7a7fa --- /dev/null +++ b/content/1_photography/1_animals/album.txt @@ -0,0 +1,19 @@ +Cover: + +- peter-fox.jpg + +---- + +Headline: Animals as leaders + +---- + +Description: What does the fox say? + +---- + +Tags: animals + +---- + +Title: Animals \ No newline at end of file diff --git a/content/1_photography/1_animals/bird-reynolds.jpg b/content/1_photography/1_animals/bird-reynolds.jpg new file mode 100644 index 0000000..d193404 Binary files /dev/null and b/content/1_photography/1_animals/bird-reynolds.jpg differ diff --git a/content/1_photography/1_animals/bird-reynolds.jpg.txt b/content/1_photography/1_animals/bird-reynolds.jpg.txt new file mode 100644 index 0000000..104be21 --- /dev/null +++ b/content/1_photography/1_animals/bird-reynolds.jpg.txt @@ -0,0 +1,25 @@ +Caption: Lewa Wildlife Conservancy, Isiolo, Kenya + +---- + +Alt: A lilac-breasted roller sitting on a branch + +---- + +Photographer: David Clode + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/7_TTPznVIQI + +---- + +Template: image + +---- + +Sort: 2 \ No newline at end of file diff --git a/content/1_photography/1_animals/dumbo.jpg b/content/1_photography/1_animals/dumbo.jpg new file mode 100644 index 0000000..38374a3 Binary files /dev/null and b/content/1_photography/1_animals/dumbo.jpg differ diff --git a/content/1_photography/1_animals/dumbo.jpg.txt b/content/1_photography/1_animals/dumbo.jpg.txt new file mode 100644 index 0000000..ce0498c --- /dev/null +++ b/content/1_photography/1_animals/dumbo.jpg.txt @@ -0,0 +1,25 @@ +Caption: + +---- + +Alt: An african elephant walking towards the camera + +---- + +Photographer: AJ Robbie + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/BuQ1RZckYW4 + +---- + +Template: image + +---- + +Sort: 3 \ No newline at end of file diff --git a/content/1_photography/1_animals/free-wheely.jpg b/content/1_photography/1_animals/free-wheely.jpg new file mode 100644 index 0000000..1e06d4e Binary files /dev/null and b/content/1_photography/1_animals/free-wheely.jpg differ diff --git a/content/1_photography/1_animals/free-wheely.jpg.txt b/content/1_photography/1_animals/free-wheely.jpg.txt new file mode 100644 index 0000000..c02b971 --- /dev/null +++ b/content/1_photography/1_animals/free-wheely.jpg.txt @@ -0,0 +1,25 @@ +Caption: + +---- + +Alt: Peacock Plumage + +---- + +Photographer: Dean Nahum + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/ecQDQb8lWDU + +---- + +Template: image + +---- + +Sort: 1 \ No newline at end of file diff --git a/content/1_photography/1_animals/peter-fox.jpg b/content/1_photography/1_animals/peter-fox.jpg new file mode 100644 index 0000000..0581bdb Binary files /dev/null and b/content/1_photography/1_animals/peter-fox.jpg differ diff --git a/content/1_photography/1_animals/peter-fox.jpg.txt b/content/1_photography/1_animals/peter-fox.jpg.txt new file mode 100644 index 0000000..d973c49 --- /dev/null +++ b/content/1_photography/1_animals/peter-fox.jpg.txt @@ -0,0 +1,25 @@ +Caption: Alaska, United States + +---- + +Alt: A fox staring at the camera + +---- + +Photographer: Sunyu + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/tIfrzHxhPYQ + +---- + +Template: image + +---- + +Sort: 5 \ No newline at end of file diff --git a/content/1_photography/1_animals/steve-turtle.jpg b/content/1_photography/1_animals/steve-turtle.jpg new file mode 100644 index 0000000..7f2a640 Binary files /dev/null and b/content/1_photography/1_animals/steve-turtle.jpg differ diff --git a/content/1_photography/1_animals/steve-turtle.jpg.txt b/content/1_photography/1_animals/steve-turtle.jpg.txt new file mode 100644 index 0000000..c10daa9 --- /dev/null +++ b/content/1_photography/1_animals/steve-turtle.jpg.txt @@ -0,0 +1,25 @@ +Caption: + +---- + +Alt: A turtle swimming under water + +---- + +Photographer: Wexor Tmg + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/L-2p8fapOA8 + +---- + +Template: image + +---- + +Sort: 6 \ No newline at end of file diff --git a/content/1_photography/2_trees/album.txt b/content/1_photography/2_trees/album.txt new file mode 100644 index 0000000..3084ee8 --- /dev/null +++ b/content/1_photography/2_trees/album.txt @@ -0,0 +1,19 @@ +Cover: + +- monster-trees-in-the-fog.jpg + +---- + +Headline: Trees - our friends with leaves + +---- + +Description: Hug them if you like. They might not appreciate it though. + +---- + +Tags: tree, forest + +---- + +Title: Trees \ No newline at end of file diff --git a/content/1_photography/2_trees/cheesy-autumn.jpg b/content/1_photography/2_trees/cheesy-autumn.jpg new file mode 100644 index 0000000..a3d324c Binary files /dev/null and b/content/1_photography/2_trees/cheesy-autumn.jpg differ diff --git a/content/1_photography/2_trees/cheesy-autumn.jpg.txt b/content/1_photography/2_trees/cheesy-autumn.jpg.txt new file mode 100644 index 0000000..885cf9e --- /dev/null +++ b/content/1_photography/2_trees/cheesy-autumn.jpg.txt @@ -0,0 +1,21 @@ +Caption: + +---- + +Alt: Colorful autumn forest + +---- + +Photographer: Federico Bottos + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/Z3NceSeZqgI + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/2_trees/last-tree-standing.jpg b/content/1_photography/2_trees/last-tree-standing.jpg new file mode 100644 index 0000000..8b40d57 Binary files /dev/null and b/content/1_photography/2_trees/last-tree-standing.jpg differ diff --git a/content/1_photography/2_trees/last-tree-standing.jpg.txt b/content/1_photography/2_trees/last-tree-standing.jpg.txt new file mode 100644 index 0000000..8689f9c --- /dev/null +++ b/content/1_photography/2_trees/last-tree-standing.jpg.txt @@ -0,0 +1,21 @@ +Caption: Pregasina, Italy + +---- + +Alt: A lonely tree above the fog + +---- + +Photographer: Cristina Gottardi + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/wVTGdIGdojc + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/2_trees/monster-trees-in-the-fog.jpg b/content/1_photography/2_trees/monster-trees-in-the-fog.jpg new file mode 100644 index 0000000..a63d940 Binary files /dev/null and b/content/1_photography/2_trees/monster-trees-in-the-fog.jpg differ diff --git a/content/1_photography/2_trees/monster-trees-in-the-fog.jpg.txt b/content/1_photography/2_trees/monster-trees-in-the-fog.jpg.txt new file mode 100644 index 0000000..96a8acf --- /dev/null +++ b/content/1_photography/2_trees/monster-trees-in-the-fog.jpg.txt @@ -0,0 +1,21 @@ +Caption: Sequoia National Forest, United States + +---- + +Alt: Huge trees reaching into the fog + +---- + +Photographer: Victoria Palacios + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/dfo06_DqxpA + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/2_trees/sharewood-forest.jpg b/content/1_photography/2_trees/sharewood-forest.jpg new file mode 100644 index 0000000..ab05840 Binary files /dev/null and b/content/1_photography/2_trees/sharewood-forest.jpg differ diff --git a/content/1_photography/2_trees/sharewood-forest.jpg.txt b/content/1_photography/2_trees/sharewood-forest.jpg.txt new file mode 100644 index 0000000..9fef665 --- /dev/null +++ b/content/1_photography/2_trees/sharewood-forest.jpg.txt @@ -0,0 +1,21 @@ +Caption: Bogd Khan Mountain, Mongolia + +---- + +Alt: Picturesque path into the forest + +---- + +Photographer: Deglee Degi + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/wQImoykAwGs + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/2_trees/stay-in-the-car.jpg b/content/1_photography/2_trees/stay-in-the-car.jpg new file mode 100644 index 0000000..ec76aaa Binary files /dev/null and b/content/1_photography/2_trees/stay-in-the-car.jpg differ diff --git a/content/1_photography/2_trees/stay-in-the-car.jpg.txt b/content/1_photography/2_trees/stay-in-the-car.jpg.txt new file mode 100644 index 0000000..24d2f78 --- /dev/null +++ b/content/1_photography/2_trees/stay-in-the-car.jpg.txt @@ -0,0 +1,21 @@ +Caption: Acadia National Park Pond, Bar Harbor, United States + +---- + +Alt: Scary forest road at night illuminated by break lights + +---- + +Photographer: Adrian Pelletier + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/OJev0ModVw8 + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/3_sky/album.txt b/content/1_photography/3_sky/album.txt new file mode 100644 index 0000000..e99606a --- /dev/null +++ b/content/1_photography/3_sky/album.txt @@ -0,0 +1,19 @@ +Title: Sky + +---- + +Cover: + +- dark-forest.jpg + +---- + +Headline: Stars and the universe and so on + +---- + +Description: + +---- + +Tags: stars, universe, up there \ No newline at end of file diff --git a/content/1_photography/3_sky/blood-moon.jpg b/content/1_photography/3_sky/blood-moon.jpg new file mode 100644 index 0000000..b513600 Binary files /dev/null and b/content/1_photography/3_sky/blood-moon.jpg differ diff --git a/content/1_photography/3_sky/blood-moon.jpg.txt b/content/1_photography/3_sky/blood-moon.jpg.txt new file mode 100644 index 0000000..0895db8 --- /dev/null +++ b/content/1_photography/3_sky/blood-moon.jpg.txt @@ -0,0 +1,25 @@ +Caption: Laguna Beach, United States + +---- + +Alt: Super blood moon + +---- + +Photographer: Derek Liang + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/XZyjr93zaZg + +---- + +Template: image + +---- + +Sort: 1 \ No newline at end of file diff --git a/content/1_photography/3_sky/coconut-milkyway.jpg b/content/1_photography/3_sky/coconut-milkyway.jpg new file mode 100644 index 0000000..496f502 Binary files /dev/null and b/content/1_photography/3_sky/coconut-milkyway.jpg differ diff --git a/content/1_photography/3_sky/coconut-milkyway.jpg.txt b/content/1_photography/3_sky/coconut-milkyway.jpg.txt new file mode 100644 index 0000000..9e8774b --- /dev/null +++ b/content/1_photography/3_sky/coconut-milkyway.jpg.txt @@ -0,0 +1,21 @@ +Caption: Maldives + +---- + +Alt: Palm trees and the night sky with stars and the milky way + +---- + +Photographer: Mohamed Ajufaan + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/Y5GuEA4ThdY + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/3_sky/dark-forest.jpg b/content/1_photography/3_sky/dark-forest.jpg new file mode 100644 index 0000000..329f62f Binary files /dev/null and b/content/1_photography/3_sky/dark-forest.jpg differ diff --git a/content/1_photography/3_sky/dark-forest.jpg.txt b/content/1_photography/3_sky/dark-forest.jpg.txt new file mode 100644 index 0000000..4535392 --- /dev/null +++ b/content/1_photography/3_sky/dark-forest.jpg.txt @@ -0,0 +1,25 @@ +Caption: Mammoth Lakes, United States + +---- + +Alt: The mikly way above mountains + +---- + +Photographer: Robson Hatsukami Morgan + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/-wEFdRCG4IU + +---- + +Template: image + +---- + +Sort: 4 \ No newline at end of file diff --git a/content/1_photography/3_sky/desert-tree.jpg b/content/1_photography/3_sky/desert-tree.jpg new file mode 100644 index 0000000..c6e7329 Binary files /dev/null and b/content/1_photography/3_sky/desert-tree.jpg differ diff --git a/content/1_photography/3_sky/desert-tree.jpg.txt b/content/1_photography/3_sky/desert-tree.jpg.txt new file mode 100644 index 0000000..d9c8a96 --- /dev/null +++ b/content/1_photography/3_sky/desert-tree.jpg.txt @@ -0,0 +1,25 @@ +Caption: Roubideau Canyon Road, Delta, United States + +---- + +Alt: A tree with the night sky in the background + +---- + +Photographer: Andrew Gloor + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/q-D_FFvnob8 + +---- + +Template: image + +---- + +Sort: 3 \ No newline at end of file diff --git a/content/1_photography/3_sky/tent-in-the-woods.jpg b/content/1_photography/3_sky/tent-in-the-woods.jpg new file mode 100644 index 0000000..8367c1e Binary files /dev/null and b/content/1_photography/3_sky/tent-in-the-woods.jpg differ diff --git a/content/1_photography/3_sky/tent-in-the-woods.jpg.txt b/content/1_photography/3_sky/tent-in-the-woods.jpg.txt new file mode 100644 index 0000000..c67fba2 --- /dev/null +++ b/content/1_photography/3_sky/tent-in-the-woods.jpg.txt @@ -0,0 +1,21 @@ +Caption: Zion National Park + +---- + +Alt: A bright lit tent in the forest with the night sky and stars above + +---- + +Photographer: Bobby Burch + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/MEBqI9fzqao + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/4_ocean/album.txt b/content/1_photography/4_ocean/album.txt new file mode 100644 index 0000000..ad1cd5f --- /dev/null +++ b/content/1_photography/4_ocean/album.txt @@ -0,0 +1,19 @@ +Cover: + +- island-from-above.jpg + +---- + +Headline: Oceans are quite nice + +---- + +Description: Blue with lots of fish + +---- + +Tags: ocean, water, blue + +---- + +Title: Ocean \ No newline at end of file diff --git a/content/1_photography/4_ocean/attention-sharks.jpg b/content/1_photography/4_ocean/attention-sharks.jpg new file mode 100644 index 0000000..da9edd4 Binary files /dev/null and b/content/1_photography/4_ocean/attention-sharks.jpg differ diff --git a/content/1_photography/4_ocean/attention-sharks.jpg.txt b/content/1_photography/4_ocean/attention-sharks.jpg.txt new file mode 100644 index 0000000..9915b65 --- /dev/null +++ b/content/1_photography/4_ocean/attention-sharks.jpg.txt @@ -0,0 +1,21 @@ +Caption: 2 Ocean Rd, Palm Beach, Australia + +---- + +Alt: A lone surfer in the waves near the beach + +---- + +Photographer: Ben Krygsman + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/g8hHM7rc-kw + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/4_ocean/island-from-above.jpg b/content/1_photography/4_ocean/island-from-above.jpg new file mode 100644 index 0000000..848352c Binary files /dev/null and b/content/1_photography/4_ocean/island-from-above.jpg differ diff --git a/content/1_photography/4_ocean/island-from-above.jpg.txt b/content/1_photography/4_ocean/island-from-above.jpg.txt new file mode 100644 index 0000000..853246a --- /dev/null +++ b/content/1_photography/4_ocean/island-from-above.jpg.txt @@ -0,0 +1,25 @@ +Caption: Breckenridge, United States + +---- + +Alt: Drone picture of a small, green island + +---- + +Photographer: Nathan Anderson + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/prIk6PdCrgg + +---- + +Template: image + +---- + +Sort: 1 \ No newline at end of file diff --git a/content/1_photography/4_ocean/jellyfish.jpg b/content/1_photography/4_ocean/jellyfish.jpg new file mode 100644 index 0000000..542baa1 Binary files /dev/null and b/content/1_photography/4_ocean/jellyfish.jpg differ diff --git a/content/1_photography/4_ocean/jellyfish.jpg.txt b/content/1_photography/4_ocean/jellyfish.jpg.txt new file mode 100644 index 0000000..577f003 --- /dev/null +++ b/content/1_photography/4_ocean/jellyfish.jpg.txt @@ -0,0 +1,25 @@ +Caption: Valencia, Spain + +---- + +Alt: A swarm of jellyfish + +---- + +Photographer: Joel Filipe + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/_AjqGGafofE + +---- + +Template: image + +---- + +Sort: 3 \ No newline at end of file diff --git a/content/1_photography/4_ocean/nasty-rocks.jpg b/content/1_photography/4_ocean/nasty-rocks.jpg new file mode 100644 index 0000000..2d2d3c6 Binary files /dev/null and b/content/1_photography/4_ocean/nasty-rocks.jpg differ diff --git a/content/1_photography/4_ocean/nasty-rocks.jpg.txt b/content/1_photography/4_ocean/nasty-rocks.jpg.txt new file mode 100644 index 0000000..cfab663 --- /dev/null +++ b/content/1_photography/4_ocean/nasty-rocks.jpg.txt @@ -0,0 +1,25 @@ +Caption: Hastings Point, Australia + +---- + +Alt: Waves crashing on rocks + +---- + +Photographer: Bailey Mahon + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/DiqUQYLov74 + +---- + +Template: image + +---- + +Sort: 2 \ No newline at end of file diff --git a/content/1_photography/4_ocean/smashed-by-waves.jpg b/content/1_photography/4_ocean/smashed-by-waves.jpg new file mode 100644 index 0000000..48b4d7d Binary files /dev/null and b/content/1_photography/4_ocean/smashed-by-waves.jpg differ diff --git a/content/1_photography/4_ocean/smashed-by-waves.jpg.txt b/content/1_photography/4_ocean/smashed-by-waves.jpg.txt new file mode 100644 index 0000000..adba4b6 --- /dev/null +++ b/content/1_photography/4_ocean/smashed-by-waves.jpg.txt @@ -0,0 +1,25 @@ +Caption: Sri Lanka, boosa + +---- + +Alt: A wave from inside + +---- + +Photographer: Maxwell Gifted + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/RbNxSL2D-xM + +---- + +Template: image + +---- + +Sort: 4 \ No newline at end of file diff --git a/content/1_photography/4_ocean/the-beach.jpg b/content/1_photography/4_ocean/the-beach.jpg new file mode 100644 index 0000000..947da33 Binary files /dev/null and b/content/1_photography/4_ocean/the-beach.jpg differ diff --git a/content/1_photography/4_ocean/the-beach.jpg.txt b/content/1_photography/4_ocean/the-beach.jpg.txt new file mode 100644 index 0000000..c647d9e --- /dev/null +++ b/content/1_photography/4_ocean/the-beach.jpg.txt @@ -0,0 +1,21 @@ +Caption: Avalon Beach, Australia + +---- + +Alt: Two surfers walking on the beach + +---- + +Photographer: Lachlan Dempsey + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/O14abKtZ5iY + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/5_desert/album.txt b/content/1_photography/5_desert/album.txt new file mode 100644 index 0000000..74f2952 --- /dev/null +++ b/content/1_photography/5_desert/album.txt @@ -0,0 +1,19 @@ +Cover: + +- indiana-jones.jpg + +---- + +Headline: The desert is pretty hot + +---- + +Description: + +---- + +Tags: desert, sand, landscape + +---- + +Title: Desert \ No newline at end of file diff --git a/content/1_photography/5_desert/area-51.jpg b/content/1_photography/5_desert/area-51.jpg new file mode 100644 index 0000000..4c727c0 Binary files /dev/null and b/content/1_photography/5_desert/area-51.jpg differ diff --git a/content/1_photography/5_desert/area-51.jpg.txt b/content/1_photography/5_desert/area-51.jpg.txt new file mode 100644 index 0000000..43e10a8 --- /dev/null +++ b/content/1_photography/5_desert/area-51.jpg.txt @@ -0,0 +1,25 @@ +Caption: + +---- + +Alt: Night sky over the desert + +---- + +Photographer: Idan Arad + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/Z0IOLvbY-qM + +---- + +Template: image + +---- + +Sort: 1 \ No newline at end of file diff --git a/content/1_photography/5_desert/death-trap.jpg b/content/1_photography/5_desert/death-trap.jpg new file mode 100644 index 0000000..6fc1726 Binary files /dev/null and b/content/1_photography/5_desert/death-trap.jpg differ diff --git a/content/1_photography/5_desert/death-trap.jpg.txt b/content/1_photography/5_desert/death-trap.jpg.txt new file mode 100644 index 0000000..f868005 --- /dev/null +++ b/content/1_photography/5_desert/death-trap.jpg.txt @@ -0,0 +1,25 @@ +Caption: Sandwich Harbour Historic, Namibia + +---- + +Alt: A dead tree in the desert + +---- + +Photographer: Ryan Cheng + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/uqYy29Sfb8Q + +---- + +Template: image + +---- + +Sort: 5 \ No newline at end of file diff --git a/content/1_photography/5_desert/dune.jpg b/content/1_photography/5_desert/dune.jpg new file mode 100644 index 0000000..e36960c Binary files /dev/null and b/content/1_photography/5_desert/dune.jpg differ diff --git a/content/1_photography/5_desert/dune.jpg.txt b/content/1_photography/5_desert/dune.jpg.txt new file mode 100644 index 0000000..140baff --- /dev/null +++ b/content/1_photography/5_desert/dune.jpg.txt @@ -0,0 +1,25 @@ +Caption: Death Valley National Park, United States + +---- + +Alt: A dune in the desert + +---- + +Photographer: Jehyun Sung + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/-Lc6azmFSk4 + +---- + +Template: image + +---- + +Sort: 2 \ No newline at end of file diff --git a/content/1_photography/5_desert/indiana-jones.jpg b/content/1_photography/5_desert/indiana-jones.jpg new file mode 100644 index 0000000..bcbd3db Binary files /dev/null and b/content/1_photography/5_desert/indiana-jones.jpg differ diff --git a/content/1_photography/5_desert/indiana-jones.jpg.txt b/content/1_photography/5_desert/indiana-jones.jpg.txt new file mode 100644 index 0000000..90289f1 --- /dev/null +++ b/content/1_photography/5_desert/indiana-jones.jpg.txt @@ -0,0 +1,25 @@ +Caption: Antelope Canyon, United States + +---- + +Alt: Antelope canyon lit by sunbeams + +---- + +Photographer: Madhu Shesharam + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/EVZxXuOEk3w + +---- + +Template: image + +---- + +Sort: 3 \ No newline at end of file diff --git a/content/1_photography/5_desert/water-please.jpg b/content/1_photography/5_desert/water-please.jpg new file mode 100644 index 0000000..e9e2695 Binary files /dev/null and b/content/1_photography/5_desert/water-please.jpg differ diff --git a/content/1_photography/5_desert/water-please.jpg.txt b/content/1_photography/5_desert/water-please.jpg.txt new file mode 100644 index 0000000..78b9e48 --- /dev/null +++ b/content/1_photography/5_desert/water-please.jpg.txt @@ -0,0 +1,25 @@ +Caption: + +---- + +Alt: The moon above the desert + +---- + +Photographer: Jordan Steranka + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/HAAq_zesf-Y + +---- + +Template: image + +---- + +Sort: 4 \ No newline at end of file diff --git a/content/1_photography/6_mountains/album.txt b/content/1_photography/6_mountains/album.txt new file mode 100644 index 0000000..dcf6a03 --- /dev/null +++ b/content/1_photography/6_mountains/album.txt @@ -0,0 +1,19 @@ +Cover: + +- probably-photoshopped.jpg + +---- + +Headline: Mountains are pretty big + +---- + +Description: Mountains — where do they come from. + +---- + +Tags: mountains, peak nature + +---- + +Title: Mountains \ No newline at end of file diff --git a/content/1_photography/6_mountains/climbers-are-crazy.jpg b/content/1_photography/6_mountains/climbers-are-crazy.jpg new file mode 100644 index 0000000..c431fa7 Binary files /dev/null and b/content/1_photography/6_mountains/climbers-are-crazy.jpg differ diff --git a/content/1_photography/6_mountains/climbers-are-crazy.jpg.txt b/content/1_photography/6_mountains/climbers-are-crazy.jpg.txt new file mode 100644 index 0000000..3a4ccb3 --- /dev/null +++ b/content/1_photography/6_mountains/climbers-are-crazy.jpg.txt @@ -0,0 +1,25 @@ +Caption: Alabama Hills, United States + +---- + +Alt: Mountain with an interesting rock formation + +---- + +Photographer: Parker Amstutz + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/4oBOCIpb3YQ + +---- + +Template: image + +---- + +Sort: 1 \ No newline at end of file diff --git a/content/1_photography/6_mountains/non-potable.jpg b/content/1_photography/6_mountains/non-potable.jpg new file mode 100644 index 0000000..59dce66 Binary files /dev/null and b/content/1_photography/6_mountains/non-potable.jpg differ diff --git a/content/1_photography/6_mountains/non-potable.jpg.txt b/content/1_photography/6_mountains/non-potable.jpg.txt new file mode 100644 index 0000000..b451f36 --- /dev/null +++ b/content/1_photography/6_mountains/non-potable.jpg.txt @@ -0,0 +1,25 @@ +Caption: Téryho chata, Vysoké Tatry, Slovakia + +---- + +Alt: A creek and mountain in the dusk + +---- + +Photographer: Štefan Štefančík + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/pmH_Y4Qetrk + +---- + +Template: image + +---- + +Sort: 2 \ No newline at end of file diff --git a/content/1_photography/6_mountains/probably-photoshopped.jpg b/content/1_photography/6_mountains/probably-photoshopped.jpg new file mode 100644 index 0000000..cb1aaa3 Binary files /dev/null and b/content/1_photography/6_mountains/probably-photoshopped.jpg differ diff --git a/content/1_photography/6_mountains/probably-photoshopped.jpg.txt b/content/1_photography/6_mountains/probably-photoshopped.jpg.txt new file mode 100644 index 0000000..a3bd24b --- /dev/null +++ b/content/1_photography/6_mountains/probably-photoshopped.jpg.txt @@ -0,0 +1,25 @@ +Caption: Ancient Bristlecone Pine Forest, United States + +---- + +Alt: Mountains in the dusk + +---- + +Photographer: John Towner + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/JgOeRuGD_Y4 + +---- + +Template: image + +---- + +Sort: 3 \ No newline at end of file diff --git a/content/1_photography/6_mountains/that-apple-mountain.jpg b/content/1_photography/6_mountains/that-apple-mountain.jpg new file mode 100644 index 0000000..e0ea6d1 Binary files /dev/null and b/content/1_photography/6_mountains/that-apple-mountain.jpg differ diff --git a/content/1_photography/6_mountains/that-apple-mountain.jpg.txt b/content/1_photography/6_mountains/that-apple-mountain.jpg.txt new file mode 100644 index 0000000..a33d106 --- /dev/null +++ b/content/1_photography/6_mountains/that-apple-mountain.jpg.txt @@ -0,0 +1,25 @@ +Caption: Yosemite National Park, USA + +---- + +Alt: The magic unfolds in Yosemite Valley + +---- + +Photographer: Madhu Shesharam + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/HRA_VAi9_Nc + +---- + +Template: image + +---- + +Sort: 4 \ No newline at end of file diff --git a/content/1_photography/6_mountains/trees-and-another-mountain.jpg b/content/1_photography/6_mountains/trees-and-another-mountain.jpg new file mode 100644 index 0000000..095c3fe Binary files /dev/null and b/content/1_photography/6_mountains/trees-and-another-mountain.jpg differ diff --git a/content/1_photography/6_mountains/trees-and-another-mountain.jpg.txt b/content/1_photography/6_mountains/trees-and-another-mountain.jpg.txt new file mode 100644 index 0000000..fc2f603 --- /dev/null +++ b/content/1_photography/6_mountains/trees-and-another-mountain.jpg.txt @@ -0,0 +1,25 @@ +Caption: Rein in Taufers, South Tyrol, Italy + +---- + +Alt: Foggy mountain with a color forest in the foreground + +---- + +Photographer: Eberhard Grossgasteiger + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/BXasVMRGsuo + +---- + +Template: image + +---- + +Sort: 5 \ No newline at end of file diff --git a/content/1_photography/7_waterfall/album.txt b/content/1_photography/7_waterfall/album.txt new file mode 100644 index 0000000..01c11be --- /dev/null +++ b/content/1_photography/7_waterfall/album.txt @@ -0,0 +1,19 @@ +Cover: + +- the-fall-is-lava.jpg + +---- + +Headline: Water that falls + +---- + +Description: Don't go chasing waterfalls + +---- + +Tags: water, falls, down + +---- + +Title: Waterfall \ No newline at end of file diff --git a/content/1_photography/7_waterfall/could-be-hawaii.jpg b/content/1_photography/7_waterfall/could-be-hawaii.jpg new file mode 100644 index 0000000..22aec6c Binary files /dev/null and b/content/1_photography/7_waterfall/could-be-hawaii.jpg differ diff --git a/content/1_photography/7_waterfall/could-be-hawaii.jpg.txt b/content/1_photography/7_waterfall/could-be-hawaii.jpg.txt new file mode 100644 index 0000000..03d25ec --- /dev/null +++ b/content/1_photography/7_waterfall/could-be-hawaii.jpg.txt @@ -0,0 +1,21 @@ +Caption: Mulafossur Waterfall, Faroe Islands + +---- + +Alt: Spectacular cliffs and a waterfall + +---- + +Photographer: Ben Tatlow + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/0-9FgC-MVfM + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/7_waterfall/lots-of-water.jpg b/content/1_photography/7_waterfall/lots-of-water.jpg new file mode 100644 index 0000000..24d91a1 Binary files /dev/null and b/content/1_photography/7_waterfall/lots-of-water.jpg differ diff --git a/content/1_photography/7_waterfall/lots-of-water.jpg.txt b/content/1_photography/7_waterfall/lots-of-water.jpg.txt new file mode 100644 index 0000000..a87c97c --- /dev/null +++ b/content/1_photography/7_waterfall/lots-of-water.jpg.txt @@ -0,0 +1,21 @@ +Caption: Aba, China + +---- + +Alt: A huge waterfall in the forest + +---- + +Photographer: Swander + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/tHWCvzEeKPw + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/7_waterfall/maybe-iceland.jpg b/content/1_photography/7_waterfall/maybe-iceland.jpg new file mode 100644 index 0000000..953bae4 Binary files /dev/null and b/content/1_photography/7_waterfall/maybe-iceland.jpg differ diff --git a/content/1_photography/7_waterfall/maybe-iceland.jpg.txt b/content/1_photography/7_waterfall/maybe-iceland.jpg.txt new file mode 100644 index 0000000..dc049f3 --- /dev/null +++ b/content/1_photography/7_waterfall/maybe-iceland.jpg.txt @@ -0,0 +1,21 @@ +Caption: Skógafoss, Iceland + +---- + +Alt: Mighty waterfall with lots of mist + +---- + +Photographer: Ruslan Valeev + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/dMB2Lh13K5w + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/7_waterfall/not-niagra.jpg b/content/1_photography/7_waterfall/not-niagra.jpg new file mode 100644 index 0000000..3df1923 Binary files /dev/null and b/content/1_photography/7_waterfall/not-niagra.jpg differ diff --git a/content/1_photography/7_waterfall/not-niagra.jpg.txt b/content/1_photography/7_waterfall/not-niagra.jpg.txt new file mode 100644 index 0000000..356aaec --- /dev/null +++ b/content/1_photography/7_waterfall/not-niagra.jpg.txt @@ -0,0 +1,21 @@ +Caption: + +---- + +Alt: Waterfall and rocks + +---- + +Photographer: Ivana Cajina + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/9LwCEYH1oW4 + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/7_waterfall/the-fall-is-lava.jpg b/content/1_photography/7_waterfall/the-fall-is-lava.jpg new file mode 100644 index 0000000..8446fa7 Binary files /dev/null and b/content/1_photography/7_waterfall/the-fall-is-lava.jpg differ diff --git a/content/1_photography/7_waterfall/the-fall-is-lava.jpg.txt b/content/1_photography/7_waterfall/the-fall-is-lava.jpg.txt new file mode 100644 index 0000000..2b0eaf3 --- /dev/null +++ b/content/1_photography/7_waterfall/the-fall-is-lava.jpg.txt @@ -0,0 +1,21 @@ +Caption: Yosemite Valley, United States + +---- + +Alt: Nicely lit waterfall in the sunset that seems like lava + +---- + +Photographer: Stephen Leonardi + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/MDmwQVgDHHM + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/7_waterfall/twin-peaks.jpg b/content/1_photography/7_waterfall/twin-peaks.jpg new file mode 100644 index 0000000..19bebc1 Binary files /dev/null and b/content/1_photography/7_waterfall/twin-peaks.jpg differ diff --git a/content/1_photography/7_waterfall/twin-peaks.jpg.txt b/content/1_photography/7_waterfall/twin-peaks.jpg.txt new file mode 100644 index 0000000..3d17c7c --- /dev/null +++ b/content/1_photography/7_waterfall/twin-peaks.jpg.txt @@ -0,0 +1,21 @@ +Caption: + +---- + +Alt: A waterfall in the forest + +---- + +Photographer: Claude Piché + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/Tqs2btwP7tQ + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/8_plants/album.txt b/content/1_photography/8_plants/album.txt new file mode 100644 index 0000000..7845b33 --- /dev/null +++ b/content/1_photography/8_plants/album.txt @@ -0,0 +1,19 @@ +Cover: + +- nice-colors-ugly-plant.jpg + +---- + +Headline: + +---- + +Description: They call me Dr. Greenthumb. + +---- + +Tags: plants + +---- + +Title: Plants \ No newline at end of file diff --git a/content/1_photography/8_plants/between-the-ferns.jpg b/content/1_photography/8_plants/between-the-ferns.jpg new file mode 100644 index 0000000..cf92f41 Binary files /dev/null and b/content/1_photography/8_plants/between-the-ferns.jpg differ diff --git a/content/1_photography/8_plants/between-the-ferns.jpg.txt b/content/1_photography/8_plants/between-the-ferns.jpg.txt new file mode 100644 index 0000000..90828b5 --- /dev/null +++ b/content/1_photography/8_plants/between-the-ferns.jpg.txt @@ -0,0 +1,21 @@ +Caption: + +---- + +Alt: Ferns in the dark + +---- + +Photographer: Annie Spratt + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/rjIacQc-uYs + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/8_plants/could-be-poisonous.jpg b/content/1_photography/8_plants/could-be-poisonous.jpg new file mode 100644 index 0000000..34fa942 Binary files /dev/null and b/content/1_photography/8_plants/could-be-poisonous.jpg differ diff --git a/content/1_photography/8_plants/could-be-poisonous.jpg.txt b/content/1_photography/8_plants/could-be-poisonous.jpg.txt new file mode 100644 index 0000000..f7dbf11 --- /dev/null +++ b/content/1_photography/8_plants/could-be-poisonous.jpg.txt @@ -0,0 +1,21 @@ +Caption: RHS Garden Wisley, Wisley, United Kingdom + +---- + +Alt: Colorful leafs + +---- + +Photographer: Annie Spratt + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/ByxgvpNYIKQ + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/8_plants/deadly-snake-hideout.jpg b/content/1_photography/8_plants/deadly-snake-hideout.jpg new file mode 100644 index 0000000..6b706c8 Binary files /dev/null and b/content/1_photography/8_plants/deadly-snake-hideout.jpg differ diff --git a/content/1_photography/8_plants/deadly-snake-hideout.jpg.txt b/content/1_photography/8_plants/deadly-snake-hideout.jpg.txt new file mode 100644 index 0000000..7f23f9b --- /dev/null +++ b/content/1_photography/8_plants/deadly-snake-hideout.jpg.txt @@ -0,0 +1,21 @@ +Caption: + +---- + +Alt: Leafs with water drops + +---- + +Photographer: Axel Holen + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/zKfumY9mQFI + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/8_plants/nice-colors-ugly-plant.jpg b/content/1_photography/8_plants/nice-colors-ugly-plant.jpg new file mode 100644 index 0000000..9ee7de2 Binary files /dev/null and b/content/1_photography/8_plants/nice-colors-ugly-plant.jpg differ diff --git a/content/1_photography/8_plants/nice-colors-ugly-plant.jpg.txt b/content/1_photography/8_plants/nice-colors-ugly-plant.jpg.txt new file mode 100644 index 0000000..9491a0b --- /dev/null +++ b/content/1_photography/8_plants/nice-colors-ugly-plant.jpg.txt @@ -0,0 +1,21 @@ +Caption: Nazareth, Israel + +---- + +Alt: Colorful succulent + +---- + +Photographer: Yousef Espanioly + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/AWYI4-h3VnM + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/8_plants/no-idea-what-that-is.jpg b/content/1_photography/8_plants/no-idea-what-that-is.jpg new file mode 100644 index 0000000..a4247b1 Binary files /dev/null and b/content/1_photography/8_plants/no-idea-what-that-is.jpg differ diff --git a/content/1_photography/8_plants/no-idea-what-that-is.jpg.txt b/content/1_photography/8_plants/no-idea-what-that-is.jpg.txt new file mode 100644 index 0000000..355ba35 --- /dev/null +++ b/content/1_photography/8_plants/no-idea-what-that-is.jpg.txt @@ -0,0 +1,21 @@ +Caption: RHS Garden Wisley, Wisley, United Kingdom + +---- + +Alt: Succulent + +---- + +Photographer: Annie Spratt + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/8mqOw4DBBSg + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/9_landscape/album.txt b/content/1_photography/9_landscape/album.txt new file mode 100644 index 0000000..38f4c96 --- /dev/null +++ b/content/1_photography/9_landscape/album.txt @@ -0,0 +1,19 @@ +Cover: + +- that-green-looks-fake.jpg + +---- + +Headline: Not a lot of portraits here + +---- + +Description: + +---- + +Tags: landscape, nature + +---- + +Title: Landscape \ No newline at end of file diff --git a/content/1_photography/9_landscape/clouds-eat-mountain.jpg b/content/1_photography/9_landscape/clouds-eat-mountain.jpg new file mode 100644 index 0000000..c35d59d Binary files /dev/null and b/content/1_photography/9_landscape/clouds-eat-mountain.jpg differ diff --git a/content/1_photography/9_landscape/clouds-eat-mountain.jpg.txt b/content/1_photography/9_landscape/clouds-eat-mountain.jpg.txt new file mode 100644 index 0000000..bc561dc --- /dev/null +++ b/content/1_photography/9_landscape/clouds-eat-mountain.jpg.txt @@ -0,0 +1,21 @@ +Caption: Licancabur + +---- + +Alt: Dry landscape with a snowy mountain in the background + +---- + +Photographer: Marcelo Quinan + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/u0ZgqJD55pE + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/9_landscape/dino-poop-mountain.jpg b/content/1_photography/9_landscape/dino-poop-mountain.jpg new file mode 100644 index 0000000..0962680 Binary files /dev/null and b/content/1_photography/9_landscape/dino-poop-mountain.jpg differ diff --git a/content/1_photography/9_landscape/dino-poop-mountain.jpg.txt b/content/1_photography/9_landscape/dino-poop-mountain.jpg.txt new file mode 100644 index 0000000..380f027 --- /dev/null +++ b/content/1_photography/9_landscape/dino-poop-mountain.jpg.txt @@ -0,0 +1,21 @@ +Caption: Iceland + +---- + +Alt: Green rocky hill + +---- + +Photographer: Cosmic Timetraveler + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/PsQzySEvx1Q + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/9_landscape/hobbits-and-stuff.jpg b/content/1_photography/9_landscape/hobbits-and-stuff.jpg new file mode 100644 index 0000000..e3b012f Binary files /dev/null and b/content/1_photography/9_landscape/hobbits-and-stuff.jpg differ diff --git a/content/1_photography/9_landscape/hobbits-and-stuff.jpg.txt b/content/1_photography/9_landscape/hobbits-and-stuff.jpg.txt new file mode 100644 index 0000000..a2f93d3 --- /dev/null +++ b/content/1_photography/9_landscape/hobbits-and-stuff.jpg.txt @@ -0,0 +1,21 @@ +Caption: Saksun, Faroe Islands + +---- + +Alt: Lake with a waterfall, surrounded by mountains + +---- + +Photographer: Ben Tatlow + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/5I8Jf7bAKuo + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/9_landscape/less-water-than-last-year.jpg b/content/1_photography/9_landscape/less-water-than-last-year.jpg new file mode 100644 index 0000000..e396bf3 Binary files /dev/null and b/content/1_photography/9_landscape/less-water-than-last-year.jpg differ diff --git a/content/1_photography/9_landscape/less-water-than-last-year.jpg.txt b/content/1_photography/9_landscape/less-water-than-last-year.jpg.txt new file mode 100644 index 0000000..8d855ec --- /dev/null +++ b/content/1_photography/9_landscape/less-water-than-last-year.jpg.txt @@ -0,0 +1,5 @@ +Caption: + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/9_landscape/mordor.jpg b/content/1_photography/9_landscape/mordor.jpg new file mode 100644 index 0000000..7afa993 Binary files /dev/null and b/content/1_photography/9_landscape/mordor.jpg differ diff --git a/content/1_photography/9_landscape/mordor.jpg.txt b/content/1_photography/9_landscape/mordor.jpg.txt new file mode 100644 index 0000000..bead0e8 --- /dev/null +++ b/content/1_photography/9_landscape/mordor.jpg.txt @@ -0,0 +1,21 @@ +Caption: The Lost Valley, Ballachulish, United Kingdom + +---- + +Alt: Eerie mountain landscape + +---- + +Photographer: Rucksack Magazine + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/358UX3A06UU + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/9_landscape/scary-ridge.jpg b/content/1_photography/9_landscape/scary-ridge.jpg new file mode 100644 index 0000000..9ec8d6b Binary files /dev/null and b/content/1_photography/9_landscape/scary-ridge.jpg differ diff --git a/content/1_photography/9_landscape/scary-ridge.jpg.txt b/content/1_photography/9_landscape/scary-ridge.jpg.txt new file mode 100644 index 0000000..7c79724 --- /dev/null +++ b/content/1_photography/9_landscape/scary-ridge.jpg.txt @@ -0,0 +1,21 @@ +Caption: Brienzer Rothorn, Flühli, Switzerland + +---- + +Alt: A ridge in the fog + +---- + +Photographer: Dave Ruck + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/AkYmPq7A4XE + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/9_landscape/that-green-looks-fake.jpg b/content/1_photography/9_landscape/that-green-looks-fake.jpg new file mode 100644 index 0000000..bdb324f Binary files /dev/null and b/content/1_photography/9_landscape/that-green-looks-fake.jpg differ diff --git a/content/1_photography/9_landscape/that-green-looks-fake.jpg.txt b/content/1_photography/9_landscape/that-green-looks-fake.jpg.txt new file mode 100644 index 0000000..737bcfc --- /dev/null +++ b/content/1_photography/9_landscape/that-green-looks-fake.jpg.txt @@ -0,0 +1,21 @@ +Caption: + +---- + +Alt: Green landscape with a few sheep + +---- + +Photographer: Jamie MacPherson + +---- + +License: Unsplash + +---- + +Link: https://unsplash.com/photos/uSJ8Xjj3ew4 + +---- + +Template: image \ No newline at end of file diff --git a/content/1_photography/photography.txt b/content/1_photography/photography.txt new file mode 100755 index 0000000..609cfad --- /dev/null +++ b/content/1_photography/photography.txt @@ -0,0 +1 @@ +Title: Photography \ No newline at end of file diff --git a/content/2_notes/20180421_across-the-ocean/note.txt b/content/2_notes/20180421_across-the-ocean/note.txt new file mode 100644 index 0000000..fdb1f16 --- /dev/null +++ b/content/2_notes/20180421_across-the-ocean/note.txt @@ -0,0 +1,31 @@ +Title: Across the ocean + +---- + +Text: + +Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. A small river named Duden flows by their place and supplies it with the necessary regelialia. It is a paradisematic country, in which roasted parts of sentences fly into your mouth. Even the all-powerful Pointing has no control about the blind texts it is an almost unorthographic life One day however a small line of blind text by the name of Lorem Ipsum decided to leave for the far World of Grammar. + +The Big Oxmox advised her not to do so, because there were thousands of bad Commas, wild Question Marks and devious Semikoli, but the Little Blind Text didn’t listen. She packed her seven versalia, put her initial into the belt and made herself on the way. When she reached the first hills of the Italic Mountains, she had a last view back on the skyline of her hometown Bookmarksgrove, the headline of Alphabet Village and the subline of her own road, the Line Lane. Pityful a rethoric question ran over her cheek, then she continued her way. On her way she met a copy. + +{{ gallery }} + +The copy warned the Little Blind Text, that where it came from it would have been rewritten a thousand times and everything that was left from its origin would be the word "and" and the Little Blind Text should turn around and return to its own, safe country. But nothing the copy said could convince her and so it didn’t take long until a few insidious Copy Writers ambushed her, made her drunk with Longe and Parole and dragged her into their agency, where they abused her for their projects again and again. And if she hasn’t been rewritten, then they are still using her. Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. + +A small river named Duden flows by their place and supplies it with the necessary regelialia. It is a paradisematic country, in which roasted parts of sentences fly into your mouth. Even the all-powerful Pointing has no control about the blind texts it is an almost unorthographic life One day however a small line of blind text by the name of Lorem Ipsum decided to leave for the far World of Grammar. The Big Oxmox advised her not to do so, because there were thousands of bad Commas, wild Question Marks and devious Semikoli, but the Little Blind Text didn’t listen. She packed her seven versalia, put her initial into the belt and made herself on the way. When she reached the first hills of the Italic Mountains, she had a last view back on the skyline of her hometown Bookmarksgrove, the headline of Alphabet Village and the subline of her own road, the Line Lane. Pityful a rethoric question ran over her cheek, then she continued her way. On her way she met a copy. The copy warned the Little Blind Text, that where it came from it would have been rewritten a thousand times and everything that was left from its origin would be the word "and" and the Little Blind Text should turn around and return to its own, safe country. But nothing the copy said could convince her and so it didn’t take long until a few insidious Copy Writers ambushed her, made her drunk + +---- + +Date: 2018-04-21 15:10 + +---- + +Author: + +---- + +Tags: ocean, pacific + +---- + +Gallery: photography/ocean \ No newline at end of file diff --git a/content/2_notes/20180625_a-night-in-the-forest/note.txt b/content/2_notes/20180625_a-night-in-the-forest/note.txt new file mode 100644 index 0000000..932cc70 --- /dev/null +++ b/content/2_notes/20180625_a-night-in-the-forest/note.txt @@ -0,0 +1,31 @@ +Title: A night in the forest + +---- + +Text: + +Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. A small river named Duden flows by their place and supplies it with the necessary regelialia. It is a paradisematic country, in which roasted parts of sentences fly into your mouth. Even the all-powerful Pointing has no control about the blind texts it is an almost unorthographic life One day however a small line of blind text by the name of Lorem Ipsum decided to leave for the far World of Grammar. + +The Big Oxmox advised her not to do so, because there were thousands of bad Commas, wild Question Marks and devious Semikoli, but the Little Blind Text didn’t listen. She packed her seven versalia, put her initial into the belt and made herself on the way. When she reached the first hills of the Italic Mountains, she had a last view back on the skyline of her hometown Bookmarksgrove, the headline of Alphabet Village and the subline of her own road, the Line Lane. Pityful a rethoric question ran over her cheek, then she continued her way. On her way she met a copy. + +{{ gallery }} + +The copy warned the Little Blind Text, that where it came from it would have been rewritten a thousand times and everything that was left from its origin would be the word "and" and the Little Blind Text should turn around and return to its own, safe country. But nothing the copy said could convince her and so it didn’t take long until a few insidious Copy Writers ambushed her, made her drunk with Longe and Parole and dragged her into their agency, where they abused her for their projects again and again. And if she hasn’t been rewritten, then they are still using her. Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. + +A small river named Duden flows by their place and supplies it with the necessary regelialia. It is a paradisematic country, in which roasted parts of sentences fly into your mouth. Even the all-powerful Pointing has no control about the blind texts it is an almost unorthographic life One day however a small line of blind text by the name of Lorem Ipsum decided to leave for the far World of Grammar. The Big Oxmox advised her not to do so, because there were thousands of bad Commas, wild Question Marks and devious Semikoli, but the Little Blind Text didn’t listen. She packed her seven versalia, put her initial into the belt and made herself on the way. When she reached the first hills of the Italic Mountains, she had a last view back on the skyline of her hometown Bookmarksgrove, the headline of Alphabet Village and the subline of her own road, the Line Lane. Pityful a rethoric question ran over her cheek, then she continued her way. On her way she met a copy. The copy warned the Little Blind Text, that where it came from it would have been rewritten a thousand times and everything that was left from its origin would be the word "and" and the Little Blind Text should turn around and return to its own, safe country. But nothing the copy said could convince her and so it didn’t take long until a few insidious Copy Writers ambushed her, made her drunk + +---- + +Date: 2018-06-25 09:30 + +---- + +Author: + +---- + +Tags: forest, trees + +---- + +Gallery: photography/trees \ No newline at end of file diff --git a/content/2_notes/20180731_in-the-jungle-of-sumatra/note.txt b/content/2_notes/20180731_in-the-jungle-of-sumatra/note.txt new file mode 100644 index 0000000..e694fa5 --- /dev/null +++ b/content/2_notes/20180731_in-the-jungle-of-sumatra/note.txt @@ -0,0 +1,31 @@ +Title: In the jungle of Sumatra + +---- + +Text: + +Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. A small river named Duden flows by their place and supplies it with the necessary regelialia. It is a paradisematic country, in which roasted parts of sentences fly into your mouth. Even the all-powerful Pointing has no control about the blind texts it is an almost unorthographic life One day however a small line of blind text by the name of Lorem Ipsum decided to leave for the far World of Grammar. + +The Big Oxmox advised her not to do so, because there were thousands of bad Commas, wild Question Marks and devious Semikoli, but the Little Blind Text didn’t listen. She packed her seven versalia, put her initial into the belt and made herself on the way. When she reached the first hills of the Italic Mountains, she had a last view back on the skyline of her hometown Bookmarksgrove, the headline of Alphabet Village and the subline of her own road, the Line Lane. Pityful a rethoric question ran over her cheek, then she continued her way. On her way she met a copy. + +{{ gallery }} + +The copy warned the Little Blind Text, that where it came from it would have been rewritten a thousand times and everything that was left from its origin would be the word "and" and the Little Blind Text should turn around and return to its own, safe country. But nothing the copy said could convince her and so it didn’t take long until a few insidious Copy Writers ambushed her, made her drunk with Longe and Parole and dragged her into their agency, where they abused her for their projects again and again. And if she hasn’t been rewritten, then they are still using her. Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. + +A small river named Duden flows by their place and supplies it with the necessary regelialia. It is a paradisematic country, in which roasted parts of sentences fly into your mouth. Even the all-powerful Pointing has no control about the blind texts it is an almost unorthographic life One day however a small line of blind text by the name of Lorem Ipsum decided to leave for the far World of Grammar. The Big Oxmox advised her not to do so, because there were thousands of bad Commas, wild Question Marks and devious Semikoli, but the Little Blind Text didn’t listen. She packed her seven versalia, put her initial into the belt and made herself on the way. When she reached the first hills of the Italic Mountains, she had a last view back on the skyline of her hometown Bookmarksgrove, the headline of Alphabet Village and the subline of her own road, the Line Lane. Pityful a rethoric question ran over her cheek, then she continued her way. On her way she met a copy. The copy warned the Little Blind Text, that where it came from it would have been rewritten a thousand times and everything that was left from its origin would be the word "and" and the Little Blind Text should turn around and return to its own, safe country. + +---- + +Date: 2018-07-31 11:05 + +---- + +Author: + +---- + +Tags: jungle, nature, sumatra, plants + +---- + +Gallery: photography/plants \ No newline at end of file diff --git a/content/2_notes/20180905_through-the-desert/note.txt b/content/2_notes/20180905_through-the-desert/note.txt new file mode 100644 index 0000000..24d1b3a --- /dev/null +++ b/content/2_notes/20180905_through-the-desert/note.txt @@ -0,0 +1,31 @@ +Title: Through the desert + +---- + +Text: + +Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. A small river named Duden flows by their place and supplies it with the necessary regelialia. It is a paradisematic country, in which roasted parts of sentences fly into your mouth. Even the all-powerful Pointing has no control about the blind texts it is an almost unorthographic life One day however a small line of blind text by the name of Lorem Ipsum decided to leave for the far World of Grammar. + +The Big Oxmox advised her not to do so, because there were thousands of bad Commas, wild Question Marks and devious Semikoli, but the Little Blind Text didn’t listen. She packed her seven versalia, put her initial into the belt and made herself on the way. When she reached the first hills of the Italic Mountains, she had a last view back on the skyline of her hometown Bookmarksgrove, the headline of Alphabet Village and the subline of her own road, the Line Lane. Pityful a rethoric question ran over her cheek, then she continued her way. On her way she met a copy. + +The copy warned the Little Blind Text, that where it came from it would have been rewritten a thousand times and everything that was left from its origin would be the word "and" and the Little Blind Text should turn around and return to its own, safe country. But nothing the copy said could convince her and so it didn’t take long until a few insidious Copy Writers ambushed her, made her drunk with Longe and Parole and dragged her into their agency, where they abused her for their projects again and again. And if she hasn’t been rewritten, then they are still using her. Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. + +{{ gallery }} + +A small river named Duden flows by their place and supplies it with the necessary regelialia. It is a paradisematic country, in which roasted parts of sentences fly into your mouth. Even the all-powerful Pointing has no control about the blind texts it is an almost unorthographic life One day however a small line of blind text by the name of Lorem Ipsum decided to leave for the far World of Grammar. The Big Oxmox advised her not to do so, because there were thousands of bad Commas, wild Question Marks and devious Semikoli, but the Little Blind Text didn’t listen. She packed her seven versalia, put her initial into the belt and made herself on the way. When she reached the first hills of the Italic Mountains, she had a last view back on the skyline of her hometown Bookmarksgrove, the headline of Alphabet Village and the subline of her own road, the Line Lane. Pityful a rethoric question ran over her cheek, then she continued her way. On her way she met a copy. The copy warned the Little Blind Text, that where it came from it would have been rewritten a thousand times and everything that was left from its origin would be the word "and" and the Little Blind Text should turn around and return to its own, safe country. But nothing the copy said could convince her and so it didn’t take long until a few insidious Copy Writers ambushed her, made her drunk + +---- + +Date: 2018-09-05 15:35 + +---- + +Author: + +---- + +Tags: sahara, desert, africa + +---- + +Gallery: photography/desert \ No newline at end of file diff --git a/content/2_notes/20180926_himalaya-and-back/note.txt b/content/2_notes/20180926_himalaya-and-back/note.txt new file mode 100644 index 0000000..1708bff --- /dev/null +++ b/content/2_notes/20180926_himalaya-and-back/note.txt @@ -0,0 +1,31 @@ +Title: Himalaya and back + +---- + +Text: + +Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. A small river named Duden flows by their place and supplies it with the necessary regelialia. It is a paradisematic country, in which roasted parts of sentences fly into your mouth. Even the all-powerful Pointing has no control about the blind texts it is an almost unorthographic life One day however a small line of blind text by the name of Lorem Ipsum decided to leave for the far World of Grammar. + +The Big Oxmox advised her not to do so, because there were thousands of bad Commas, wild Question Marks and devious Semikoli, but the Little Blind Text didn’t listen. She packed her seven versalia, put her initial into the belt and made herself on the way. When she reached the first hills of the Italic Mountains, she had a last view back on the skyline of her hometown Bookmarksgrove, the headline of Alphabet Village and the subline of her own road, the Line Lane. Pityful a rethoric question ran over her cheek, then she continued her way. On her way she met a copy. + +{{ gallery }} + +The copy warned the Little Blind Text, that where it came from it would have been rewritten a thousand times and everything that was left from its origin would be the word "and" and the Little Blind Text should turn around and return to its own, safe country. But nothing the copy said could convince her and so it didn’t take long until a few insidious Copy Writers ambushed her, made her drunk with Longe and Parole and dragged her into their agency, where they abused her for their projects again and again. And if she hasn’t been rewritten, then they are still using her. Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. + +A small river named Duden flows by their place and supplies it with the necessary regelialia. It is a paradisematic country, in which roasted parts of sentences fly into your mouth. Even the all-powerful Pointing has no control about the blind texts it is an almost unorthographic life One day however a small line of blind text by the name of Lorem Ipsum decided to leave for the far World of Grammar. The Big Oxmox advised her not to do so, because there were thousands of bad Commas, wild Question Marks and devious Semikoli, but the Little Blind Text didn’t listen. She packed her seven versalia, put her initial into the belt and made herself on the way. When she reached the first hills of the Italic Mountains, she had a last view back on the skyline of her hometown Bookmarksgrove, the headline of Alphabet Village and the subline of her own road, the Line Lane. Pityful a rethoric question ran over her cheek, then she continued her way. On her way she met a copy. The copy warned the Little Blind Text, that where it came from it would have been rewritten a thousand times and everything that was left from its origin would be the word "and" and the Little Blind Text should turn around and return to its own, safe country. But nothing the copy said could convince her and so it didn’t take long until a few insidious Copy Writers ambushed her, made her drunk + +---- + +Date: 2018-09-26 17:30 + +---- + +Author: + +---- + +Tags: mountains, nepal + +---- + +Gallery: photography/mountains \ No newline at end of file diff --git a/content/2_notes/20181005_chasing-waterfalls/note.txt b/content/2_notes/20181005_chasing-waterfalls/note.txt new file mode 100644 index 0000000..aac34f7 --- /dev/null +++ b/content/2_notes/20181005_chasing-waterfalls/note.txt @@ -0,0 +1,33 @@ +Title: Chasing waterfalls + +---- + +Text: + +Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. A small river named Duden flows by their place and supplies it with the necessary regelialia. It is a paradisematic country, in which roasted parts of sentences fly into your mouth. Even the all-powerful Pointing has no control about the blind texts it is an almost unorthographic life One day however a small line of blind text by the name of Lorem Ipsum decided to leave for the far World of Grammar. + +The Big Oxmox advised her not to do so, because there were thousands of bad Commas, wild Question Marks and devious Semikoli, but the Little Blind Text didn’t listen. She packed her seven versalia, put her initial into the belt and made herself on the way. When she reached the first hills of the Italic Mountains, she had a last view back on the skyline of her hometown Bookmarksgrove, the headline of Alphabet Village and the subline of her own road, the Line Lane. Pityful a rethoric question ran over her cheek, then she continued her way. On her way she met a copy. + +The copy warned the Little Blind Text, that where it came from it would have been rewritten a thousand times and everything that was left from its origin would be the word "and" and the Little Blind Text should turn around and return to its own, safe country. But nothing the copy said could convince her and so it didn’t take long until a few insidious Copy Writers ambushed her, made her drunk with Longe and Parole and dragged her into their agency, where they abused her for their projects again and again. And if she hasn’t been rewritten, then they are still using her. Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. + +{{ gallery }} + +A small river named Duden flows by their place and supplies it with the necessary regelialia. It is a paradisematic country, in which roasted parts of sentences fly into your mouth. Even the all-powerful Pointing has no control about the blind texts it is an almost unorthographic life One day however a small line of blind text by the name of Lorem Ipsum decided to leave for the far World of Grammar. The Big Oxmox advised her not to do so, because there were thousands of bad Commas, wild Question Marks and devious Semikoli, but the Little Blind Text didn’t listen. She packed her seven versalia, put her initial into the belt and made herself on the way. When she reached the first hills of the Italic Mountains, she had a last view back on the skyline of her hometown Bookmarksgrove, the headline of Alphabet Village and the subline of her own road, the Line Lane. Pityful a rethoric question ran over her cheek, then she continued her way. On her way she met a copy. The copy warned the Little Blind Text, that where it came from it would have been rewritten a thousand times and everything that was left from its origin would be the word "and" and the Little Blind Text should turn around and return to its own, safe country. But nothing the copy said could convince her and so it didn’t take long until a few insidious Copy Writers ambushed her, made her drunk + +---- + +Date: 2018-10-05 10:40 + +---- + +Author: + +---- + +Tags: water, nature, waterfalls, landscape + +---- + +Gallery: + +- photography/waterfall \ No newline at end of file diff --git a/content/2_notes/20181031_exploring-the-universe/note.txt b/content/2_notes/20181031_exploring-the-universe/note.txt new file mode 100644 index 0000000..02733ab --- /dev/null +++ b/content/2_notes/20181031_exploring-the-universe/note.txt @@ -0,0 +1,35 @@ +Title: Exploring the universe + +---- + +Text: + +Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. A small river named Duden flows by their place and supplies it with the necessary regelialia. It is a paradisematic country, in which roasted parts of sentences fly into your mouth. Even the all-powerful Pointing has no control about the blind texts it is an almost unorthographic life One day however a small line of blind text by the name of Lorem Ipsum decided to leave for the far World of Grammar. + +The Big Oxmox advised her not to do so, because there were thousands of bad Commas, wild Question Marks and devious Semikoli, but the Little Blind Text didn’t listen. She packed her seven versalia, put her initial into the belt and made herself on the way. When she reached the first hills of the Italic Mountains, she had a last view back on the skyline of her hometown Bookmarksgrove, the headline of Alphabet Village and the subline of her own road, the Line Lane. Pityful a rethoric question ran over her cheek, then she continued her way. On her way she met a copy. + +{{ gallery }} + +The copy warned the Little Blind Text, that where it came from it would have been rewritten a thousand times and everything that was left from its origin would be the word "and" and the Little Blind Text should turn around and return to its own, safe country. But nothing the copy said could convince her and so it didn’t take long until a few insidious Copy Writers ambushed her, made her drunk with Longe and Parole and dragged her into their agency, where they abused her for their projects again and again. And if she hasn’t been rewritten, then they are still using her. Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. + +---- + +Date: 2018-10-31 13:15 + +---- + +Author: + +---- + +Tags: universe, stars, sky + +---- + +Gallery: + +- photography/animals + +---- + +Headline: Super nicer scheiss \ No newline at end of file diff --git a/content/2_notes/notes.txt b/content/2_notes/notes.txt new file mode 100755 index 0000000..42f3725 --- /dev/null +++ b/content/2_notes/notes.txt @@ -0,0 +1 @@ +Title: Notes diff --git a/content/3_about/about.txt b/content/3_about/about.txt new file mode 100644 index 0000000..f27500b --- /dev/null +++ b/content/3_about/about.txt @@ -0,0 +1,46 @@ +Text: + +Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. A small river named Duden flows by their place and supplies it with the necessary regelialia. It is a paradisematic country, in which roasted parts of sentences fly into your mouth. Even the all-powerful Pointing has no control about the blind texts it is an almost unorthographic life One day however a small line of blind text by the name of Lorem Ipsum decided to leave for the far World of Grammar. + +The Big Oxmox advised her not to do so, because there were thousands of bad Commas, wild Question Marks and devious Semikoli, but the Little Blind Text didn’t listen. She packed her seven versalia, put her initial into the belt and made herself on the way. When she reached the first hills of the Italic Mountains, she had a last view back on the skyline of her hometown Bookmarksgrove, the headline of Alphabet Village and the subline of her own road, the Line Lane. Pityful a rethoric question ran over her cheek, then she continued her way. On her way she met a copy. + +---- + +Address: + +Mægazine Inc. +Sesamestreet 1 +Gotham City +USA + +---- + +Email: mail@maegazine.com + +---- + +Phone: +49 1234 5678 + +---- + +Social: + +- + platform: Twitter + url: https://twitter.com/getkirby +- + platform: Instagram + url: https://instagram.com/getkirby + +---- + +Contact: + +Kirby Magazine Inc. +Sesamestreet 1 +Gotham City +USA + +---- + +Title: About us diff --git a/content/error/error.txt b/content/error/error.txt new file mode 100755 index 0000000..ec6373b --- /dev/null +++ b/content/error/error.txt @@ -0,0 +1,9 @@ +Title: Error + +---- + +Intro: The page could not be found. + +---- + +Text: Well, stuff can go wrong sometimes and maybe you mistyped a URL, tapped/clicked on a broken link or your computer just wants to drive you nuts. Anyway … keep calm and take a look at our beautiful (link: / text: homepage) instead. diff --git a/content/home/home.txt b/content/home/home.txt new file mode 100755 index 0000000..1791fc5 --- /dev/null +++ b/content/home/home.txt @@ -0,0 +1 @@ +Title: Home diff --git a/content/sandbox/sandbox.txt b/content/sandbox/sandbox.txt new file mode 100644 index 0000000..f4a8a1e --- /dev/null +++ b/content/sandbox/sandbox.txt @@ -0,0 +1,5 @@ +Test: + +---- + +Title: Sandbox \ No newline at end of file diff --git a/content/site.txt b/content/site.txt new file mode 100755 index 0000000..0d7ee9c --- /dev/null +++ b/content/site.txt @@ -0,0 +1 @@ +Title: Mægazine diff --git a/index.php b/index.php new file mode 100644 index 0000000..9b08443 --- /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/README.md b/kirby/README.md new file mode 100755 index 0000000..53d79ed --- /dev/null +++ b/kirby/README.md @@ -0,0 +1,25 @@ +# Kirby + +[![Build Status](https://travis-ci.com/k-next/kirby.svg?branch=master)](https://travis-ci.com/k-next/kirby) +[![Coverage Status](https://coveralls.io/repos/github/k-next/kirby/badge.svg?branch=master)](https://coveralls.io/github/k-next/kirby?branch=master) + +This is Kirby's core application folder. Get started with one of the following repositories instead: + +- [Starterkit](https://github.com/getkirby/starterkit) +- [Plainkit](https://github.com/getkirby/plainkit) + +## Bug reports + +Please post all bug reports in our issue tracker: +https://github.com/getkirby/kirby/issues + +## Feature suggestions + +If you want to suggest features or enhancements for Kirby, please use our Ideas repository: +https://github.com/getkirby/ideas/issues + +## License + +Kirby is not free software. In order to run Kirby on a public server you must purchase a valid license. +- https://getkirby.com/buy +- https://getkirby.com/license diff --git a/kirby/bootstrap.php b/kirby/bootstrap.php new file mode 100755 index 0000000..d1cd6cc --- /dev/null +++ b/kirby/bootstrap.php @@ -0,0 +1,34 @@ +') === 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 + */ +} + +define('DS', '/'); diff --git a/kirby/composer.json b/kirby/composer.json new file mode 100755 index 0000000..fa321c2 --- /dev/null +++ b/kirby/composer.json @@ -0,0 +1,52 @@ +{ + "name": "getkirby/cms", + "description": "The Kirby 3 core", + "version": "3.0.0-RC-5.0", + "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": "*", + "mustangostang/spyc": "0.6.*", + "michelf/php-smartypants": "1.8.*", + "claviska/simpleimage": "3.3.*", + "phpmailer/phpmailer": "6.0.*", + "filp/whoops": "2.3.*", + "true/punycode": "2.1.*", + "zendframework/zend-escaper": "2.6.*" + }, + "autoload": { + "files": ["config/helpers.php", "config/aliases.php", "config/tests.php"], + "classmap": ["dependencies/"], + "psr-4": { + "Kirby\\": "src/" + } + }, + "scripts": { + "analyze": "phpstan analyse src", + "test": "phpunit --stderr --coverage-html=tests/coverage", + "zip": "composer archive --format=zip --file=dist", + "build": "./scripts/build", + "fix": "php-cs-fixer fix ." + }, + "config": { + "optimize-autoloader": true + } +} diff --git a/kirby/composer.lock b/kirby/composer.lock new file mode 100755 index 0000000..24669d4 --- /dev/null +++ b/kirby/composer.lock @@ -0,0 +1,577 @@ +{ + "_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": "0f4c41d639d0273e46924acd006c47cc", + "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.0.1", + "source": { + "type": "git", + "url": "https://github.com/k-next/composer-installer.git", + "reference": "dc0e38c4f0fc04875c1a523d82364a44f436cbf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/k-next/composer-installer/zipball/dc0e38c4f0fc04875c1a523d82364a44f436cbf4", + "reference": "dc0e38c4f0fc04875c1a523d82364a44f436cbf4", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0" + }, + "require-dev": { + "composer/composer": "^1.3", + "phpunit/phpunit": "^6.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Kirby\\ComposerInstaller\\Plugin" + }, + "autoload": { + "psr-4": { + "Kirby\\ComposerInstaller\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Kirby's custom Composer installer for the Kirby CMS", + "homepage": "https://getkirby.com", + "time": "2018-12-19T10:01:45+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.6", + "source": { + "type": "git", + "url": "https://github.com/PHPMailer/PHPMailer.git", + "reference": "8190d73eb5def11a43cfb020b7f36db65330698c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/8190d73eb5def11a43cfb020b7f36db65330698c", + "reference": "8190d73eb5def11a43cfb020b7f36db65330698c", + "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": "2018-11-16T00:41:32+00:00" + }, + { + "name": "psr/log", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.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": "2018-11-20T15:27:04+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "c79c051f5b3a46be09205c73b80b346e4153e494" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494", + "reference": "c79c051f5b3a46be09205c73b80b346e4153e494", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-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": "2018-09-21T13:07:52+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" + }, + { + "name": "zendframework/zend-escaper", + "version": "2.6.0", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-escaper.git", + "reference": "31d8aafae982f9568287cb4dce987e6aff8fd074" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-escaper/zipball/31d8aafae982f9568287cb4dce987e6aff8fd074", + "reference": "31d8aafae982f9568287cb4dce987e6aff8fd074", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2", + "zendframework/zend-coding-standard": "~1.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6.x-dev", + "dev-develop": "2.7.x-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\Escaper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs", + "keywords": [ + "ZendFramework", + "escaper", + "zf" + ], + "time": "2018-04-25T15:48:53+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..0b82e6a --- /dev/null +++ b/kirby/config/aliases.php @@ -0,0 +1,65 @@ + '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', + + // 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' +]; + +spl_autoload_register(function ($class) use ($aliases) { + $class = strtolower($class); + + if (isset($aliases[$class]) === true) { + class_alias($aliases[$class], $class); + } +}); diff --git a/kirby/config/api/authentication.php b/kirby/config/api/authentication.php new file mode 100755 index 0000000..8c31128 --- /dev/null +++ b/kirby/config/api/authentication.php @@ -0,0 +1,25 @@ +kirby()->auth(); + + // csrf token check + if ($auth->type() === 'session' && $auth->csrf() === false) { + throw new PermissionException('Unauthenticated', 403); + } + + // 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', 403); +}; diff --git a/kirby/config/api/collections.php b/kirby/config/api/collections.php new file mode 100755 index 0000000..bafa483 --- /dev/null +++ b/kirby/config/api/collections.php @@ -0,0 +1,80 @@ + [ + 'model' => 'page', + 'type' => Pages::class, + 'view' => 'compact' + ], + + /** + * Files + */ + 'files' => [ + 'model' => 'file', + 'type' => Files::class + ], + + /** + * Languages + */ + 'languages' => [ + 'model' => 'language', + 'type' => Languages::class, + 'view' => 'compact' + ], + + /** + * Pages + */ + 'pages' => [ + 'model' => 'page', + 'type' => Pages::class, + 'view' => 'compact' + ], + + /** + * Roles + */ + 'roles' => [ + 'model' => 'role', + 'type' => Roles::class, + 'view' => 'compact' + ], + + /** + * Translations + */ + 'translations' => [ + 'model' => 'translation', + 'type' => Translations::class, + 'view' => 'compact' + ], + + /** + * Users + */ + 'users' => [ + 'default' => function () { + return $this->users(); + }, + 'model' => 'user', + 'type' => Users::class, + 'view' => 'compact' + ] + +]; diff --git a/kirby/config/api/models.php b/kirby/config/api/models.php new file mode 100755 index 0000000..5e02abf --- /dev/null +++ b/kirby/config/api/models.php @@ -0,0 +1,24 @@ + 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..09ddcf4 --- /dev/null +++ b/kirby/config/api/models/File.php @@ -0,0 +1,160 @@ + [ + 'blueprint' => function (File $file) { + return $file->blueprint(); + }, + 'content' => function (File $file) { + return Form::for($file)->values(); + }, + 'dimensions' => function (File $file) { + return $file->dimensions()->toArray(); + }, + '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'); + $index = $files->indexOf($file); + + return $files->nth($index + 1); + }, + 'options' => function (File $file) { + return $file->permissions()->toArray(); + }, + 'prev' => function (File $file) { + return $file->prev(); + }, + 'prevWithTemplate' => function (File $file) { + $files = $file->templateSiblings()->sortBy('sort', 'asc'); + $index = $files->indexOf($file); + + return $files->nth($index - 1); + }, + 'niceSize' => function (File $file) { + return $file->niceSize(); + }, + 'panelIcon' => function (File $file) { + return $file->panelIcon(); + }, + 'panelImage' => function (File $file) { + return $file->panelImage(); + }, + 'parent' => function (File $file) { + return $file->parent(); + }, + 'parents' => function (File $file) { + return $file->parents()->flip(); + }, + 'template' => function (File $file) { + return $file->template(); + }, + 'size' => function (File $file) { + return $file->size(); + }, + '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' => File::class, + '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..52588f2 --- /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' => FileBlueprint::class, + 'views' => [ + ], +]; diff --git a/kirby/config/api/models/FileVersion.php b/kirby/config/api/models/FileVersion.php new file mode 100755 index 0000000..6b7509b --- /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' => FileVersion::class, + '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..0134883 --- /dev/null +++ b/kirby/config/api/models/Language.php @@ -0,0 +1,37 @@ + [ + '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(); + }, + 'url' => function (Language $language) { + return $language->url(); + }, + ], + 'type' => Language::class, + 'views' => [ + 'compact' => [ + 'code', + 'default', + 'name', + ] + ] +]; diff --git a/kirby/config/api/models/Page.php b/kirby/config/api/models/Page.php new file mode 100755 index 0000000..5a87a96 --- /dev/null +++ b/kirby/config/api/models/Page.php @@ -0,0 +1,155 @@ + [ + '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(); + }, + 'hasChildren' => function (Page $page) { + return $page->hasChildren(); + }, + 'hasDrafts' => function (Page $page) { + return $page->hasDrafts(); + }, + '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->permissions()->toArray(); + }, + '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' => Page::class, + 'views' => [ + 'compact' => [ + 'id', + 'title', + 'url', + 'num' + ], + 'default' => [ + 'content', + 'id', + 'status', + 'num', + 'options', + 'parent' => 'compact', + 'slug', + 'template', + 'title', + 'url' + ], + 'panel' => [ + 'id', + 'blueprint', + 'content', + 'errors', + '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..fdb5ccc --- /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' => PageBlueprint::class, + 'views' => [ + ], +]; diff --git a/kirby/config/api/models/Role.php b/kirby/config/api/models/Role.php new file mode 100755 index 0000000..7613a09 --- /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' => Role::class, + '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..a5983b6 --- /dev/null +++ b/kirby/config/api/models/Site.php @@ -0,0 +1,65 @@ + 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(); + }, + 'files' => function (Site $site) { + return $site->files(); + }, + 'options' => function (Site $site) { + return $site->permissions()->toArray(); + }, + 'title' => function (Site $site) { + return $site->title()->value(); + }, + 'url' => function (Site $site) { + return $site->url(); + }, + ], + 'type' => Site::class, + 'views' => [ + 'compact' => [ + 'title', + 'url' + ], + 'default' => [ + 'content', + 'options', + 'title', + 'url' + ], + 'panel' => [ + 'title', + 'blueprint', + 'content', + 'options', + '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..b7a45d6 --- /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' => SiteBlueprint::class, + 'views' => [ + ], +]; diff --git a/kirby/config/api/models/System.php b/kirby/config/api/models/System.php new file mode 100755 index 0000000..d9ac98f --- /dev/null +++ b/kirby/config/api/models/System.php @@ -0,0 +1,95 @@ + [ + '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(); + }, + 'breadcrumbTitle' => function () { + return $this->site()->blueprint()->title(); + }, + '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' => System::class, + 'views' => [ + 'login' => [ + 'isOk', + 'isInstalled', + 'title', + 'translation' + ], + 'troubleshooting' => [ + 'isOk', + 'isInstallable', + 'isInstalled', + 'title', + 'translation', + 'requirements' + ], + 'panel' => [ + 'breadcrumbTitle', + 'isOk', + 'isInstalled', + 'isLocal', + 'kirbytext', + 'languages' => 'compact', + 'license', + 'multilang', + 'requirements', + '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..2f72efc --- /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' => Translation::class, + '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..33c88ca --- /dev/null +++ b/kirby/config/api/models/User.php @@ -0,0 +1,102 @@ + 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(); + }, + '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->permissions()->toArray(); + }, + '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' => User::class, + '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..48cb1f4 --- /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' => UserBlueprint::class, + 'views' => [ + ], +]; diff --git a/kirby/config/api/routes.php b/kirby/config/api/routes.php new file mode 100755 index 0000000..1d6bdf6 --- /dev/null +++ b/kirby/config/api/routes.php @@ -0,0 +1,25 @@ +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..fe03d62 --- /dev/null +++ b/kirby/config/api/routes/auth.php @@ -0,0 +1,58 @@ + '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'); + + if ($user = $this->kirby()->auth()->login($email, $password, $long)) { + return [ + 'code' => 200, + 'status' => 'ok', + 'user' => $this->resolve($user)->view('auth')->toArray() + ]; + } + + throw new InvalidArgumentException('Invalid email or password'); + } + ], + [ + '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..3b78fbb --- /dev/null +++ b/kirby/config/api/routes/files.php @@ -0,0 +1,100 @@ + '(:all)/files', + 'method' => 'GET', + 'action' => function (string $path) { + return $this->parent($path)->files(); + } + ], + [ + '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' => 'POST', + 'action' => function (string $path) { + return $this->parent($path)->files()->query($this->requestBody()); + } + ], + [ + 'pattern' => '(:all)/files/sort', + 'method' => 'PATCH', + 'action' => function (string $path) { + return $this->parent($path)->files()->changeSort($this->requestBody('files')); + } + ], + [ + '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')); + } + ], + [ + 'pattern' => '(: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); + } + } + ] + +]; 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/pages.php b/kirby/config/api/routes/pages.php new file mode 100755 index 0000000..97e2fce --- /dev/null +++ b/kirby/config/api/routes/pages.php @@ -0,0 +1,105 @@ + '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' => 'POST', + 'action' => function (string $id) { + return $this->page($id)->children()->query($this->requestBody()); + } + ], + [ + '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..340c0a4 --- /dev/null +++ b/kirby/config/api/routes/roles.php @@ -0,0 +1,21 @@ + 'roles', + 'method' => 'GET', + 'action' => function () { + 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..6a1cfd6 --- /dev/null +++ b/kirby/config/api/routes/site.php @@ -0,0 +1,95 @@ + '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', + 'action' => function () { + return $this->site() + ->index(true) + ->filterBy('isReadable', true) + ->search($this->requestQuery('q'), [ + 'score' => [ + 'id' => 64, + 'title' => 64, + ] + ]); + } + ], + [ + '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..3498410 --- /dev/null +++ b/kirby/config/api/routes/system.php @@ -0,0 +1,72 @@ + '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->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..e1f5953 --- /dev/null +++ b/kirby/config/api/routes/users.php @@ -0,0 +1,137 @@ + 'users', + 'method' => 'GET', + 'action' => function () { + return $this->users(); + } + ], + [ + 'pattern' => 'users', + 'method' => 'POST', + 'action' => function () { + return $this->users()->create($this->requestBody()); + } + ], + [ + 'pattern' => 'users/search', + 'method' => 'POST', + 'action' => function () { + 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..e0d2fd0 --- /dev/null +++ b/kirby/config/components.php @@ -0,0 +1,95 @@ + function (App $kirby, Model $file, array $options = []) { + if ($file->isResizable() === false) { + return $file; + } + + // pre-calculate all thumb attributes + $darkroom = Darkroom::factory(option('thumbs.driver', 'gd'), option('thumbs', [])); + $attributes = $darkroom->preprocess($file->root(), $options); + + // create url and root + $parent = $file->parent(); + $mediaRoot = $parent->mediaRoot() . '/' . $file->mediaHash(); + $dst = $mediaRoot . '/{{ name }}{{ attributes }}.{{ extension }}'; + $thumbRoot = (new Filename($file->root(), $dst, $attributes))->toString(); + $thumbName = basename($thumbRoot); + $job = $mediaRoot . '/.jobs/' . $thumbName . '.json'; + + if (file_exists($thumbRoot) === false) { + try { + Data::write($job, array_merge($attributes, [ + 'filename' => $file->filename() + ])); + } catch (Throwable $e) { + return $file; + } + } + + return new FileVersion([ + 'modifications' => $options, + 'original' => $file, + 'root' => $thumbRoot, + 'url' => $parent->mediaUrl() . '/' . $file->mediaHash() . '/' . $thumbName, + ]); + }, + 'file::url' => function (App $kirby, Model $file) { + return $file->mediaUrl(); + }, + 'markdown' => function (App $kirby, string $text = null, array $options = []): string { + static $markdown; + + if (isset($markdown) === false) { + $parser = ($options['extra'] ?? false) === true ? 'ParsedownExtra' : 'Parsedown'; + $markdown = new $parser; + $markdown->setBreaksEnabled($options['breaks'] ?? true); + } + + // we need the @ here, because parsedown has some notice issues :( + return @$markdown->text($text); + }, + 'smartypants' => function (App $kirby, string $text = null, array $options = []): string { + static $smartypants; + + $smartypants = $smartypants ?? new Smartypants($options); + + return $smartypants->parse($text); + }, + 'snippet' => function (App $kirby, string $name, array $data = []) { + $file = $kirby->root('snippets') . '/' . $name . '.php'; + + if (file_exists($file) === false) { + $file = $kirby->extensions('snippets')[$name] ?? null; + } + + return Snippet::load($file, $data); + }, + 'template' => function (App $kirby, string $name, string $type = 'html', string $defaultType = 'html') { + return new Template($name, $type, $defaultType); + }, + 'thumb' => function (App $kirby, string $src, string $dst, array $options) { + $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); + $darkroom->process($root, $options); + + return $root; + }, +]; 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..f45c50e --- /dev/null +++ b/kirby/config/fields/checkboxes.php @@ -0,0 +1,64 @@ + ['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' => [ + 'options' => function (): array { + return $this->getOptions(); + }, + '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..d4a7776 --- /dev/null +++ b/kirby/config/fields/date.php @@ -0,0 +1,76 @@ + [ + /** + * Default date when a new Page/File/User gets created + */ + 'default' => function ($default = null) { + return $default; + }, + /** + * 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(DATE_W3C, $timestamp); + } + + return null; + } + ], + 'save' => function ($value) { + if ($value !== null && $date = strtotime($value)) { + return date($this->format(), $date); + } + + return ''; + }, + 'validations' => [ + 'date' + ] +]; diff --git a/kirby/config/fields/email.php b/kirby/config/fields/email.php new file mode 100755 index 0000000..1772ae3 --- /dev/null +++ b/kirby/config/fields/email.php @@ -0,0 +1,38 @@ + '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..db24799 --- /dev/null +++ b/kirby/config/fields/files.php @@ -0,0 +1,188 @@ + [ + /** + * 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; + }, + + /** + * The placeholder text if no pages have been selected yet + */ + 'empty' => function ($empty = null) { + return I18n::translate($empty, $empty); + }, + + /** + * Image settings for each item + */ + 'image' => function (array $image = null) { + return $image ?? []; + }, + + /** + * Info text + */ + 'info' => function (string $info = null) { + return $info; + }, + + /** + * Changes the layout of the selected files. Available layouts: list, cards + */ + 'layout' => function (string $layout = 'list') { + return $layout; + }, + + /** + * Minimum number of required files + */ + 'min' => function (int $min = null) { + return $min; + }, + + /** + * Maximum number of allowed files + */ + 'max' => function (int $max = null) { + return $max; + }, + + /** + * If false, only a single file can be selected + */ + 'multiple' => function (bool $multiple = true) { + return $multiple; + }, + + /** + * Query for the files to be included + */ + 'query' => function (string $query = 'page.files') { + return $query; + }, + + /** + * Layout size for cards + */ + 'size' => function (string $size = null) { + return $size; + }, + + /** + * Main text + */ + 'text' => function (string $text = '{{ file.filename }}') { + return $text; + }, + + '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); + }, + 'default' => function () { + return $this->toFiles($this->default); + }, + 'value' => function () { + return $this->toFiles($this->value); + }, + ], + 'methods' => [ + 'fileResponse' => function ($file) { + if ($this->layout === 'list') { + $thumb = [ + 'width' => 100, + 'height' => 100 + ]; + } else { + $thumb = [ + 'width' => 400, + 'height' => 400 + ]; + } + + $image = $file->panelImage($this->image, $thumb); + $model = $this->model(); + $uuid = $file->parent() === $model ? $file->filename() : $file->id(); + + return [ + 'filename' => $file->filename(), + 'text' => $file->toString($this->text), + 'link' => $file->panelUrl(true), + 'id' => $file->id(), + 'uuid' => $uuid, + 'url' => $file->url(), + 'info' => $file->toString($this->info ?? false), + 'image' => $image, + 'icon' => $file->panelIcon($image), + 'type' => $file->type(), + ]; + }, + 'toFiles' => function ($value = null) { + $files = []; + $kirby = kirby(); + + 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(); + $files = $field->model()->query($field->query(), 'Kirby\Cms\Files'); + $data = []; + + foreach ($files as $index => $file) { + $data[] = $field->fileResponse($file); + } + + return $data; + } + ] + ]; + }, + '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..44a70de --- /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' => function ($value = null) { + return I18n::translate($value, $value); + }, + ], + 'computed' => [ + 'text' => function () { + $text = $this->text; + + if ($model = $this->model()) { + $text = $this->model()->toString($text); + } + + return kirbytext($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/options.php b/kirby/config/fields/mixins/options.php new file mode 100755 index 0000000..42618cb --- /dev/null +++ b/kirby/config/fields/mixins/options.php @@ -0,0 +1,43 @@ + [ + /** + * 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; + }, + ], + '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/multiselect.php b/kirby/config/fields/multiselect.php new file mode 100755 index 0000000..0d357a5 --- /dev/null +++ b/kirby/config/fields/multiselect.php @@ -0,0 +1,25 @@ + 'tags', + 'props' => [ + /** + * 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, entries will be sorted alphabetically on selection + */ + '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..6b31759 --- /dev/null +++ b/kirby/config/fields/number.php @@ -0,0 +1,49 @@ + [ + /** + * 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 = 1) { + return $this->toNumber($step); + }, + 'value' => function ($value = null) { + return $this->toNumber($value); + } + ], + 'methods' => [ + 'toNumber' => function ($value) { + if ($this->isEmpty($value) === true) { + return null; + } + + $value = str_replace(',', '.', $value); + $value = floatval($value); + + return $value; + } + ], + 'validations' => [ + 'min', + 'max' + ] +]; diff --git a/kirby/config/fields/pages.php b/kirby/config/fields/pages.php new file mode 100755 index 0000000..7004eca --- /dev/null +++ b/kirby/config/fields/pages.php @@ -0,0 +1,188 @@ + [ + /** + * 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); + }, + + /** + * The placeholder text if no pages have been selected yet + */ + 'empty' => function ($empty = null) { + return I18n::translate($empty, $empty); + }, + + /** + * Image settings for each item + */ + 'image' => function (array $image = null) { + return $image ?? []; + }, + + /** + * Info text + */ + 'info' => function (string $info = null) { + return $info; + }, + + /** + * Changes the layout of the selected files. Available layouts: list, cards + */ + 'layout' => function (string $layout = 'list') { + return $layout; + }, + + /** + * The minimum number of required selected pages + */ + 'min' => function (int $min = null) { + return $min; + }, + + /** + * The maximum number of allowed selected pages + */ + 'max' => function (int $max = null) { + return $max; + }, + + /** + * If false, only a single page can be selected + */ + 'multiple' => function (bool $multiple = true) { + return $multiple; + }, + + /** + * Optional query to select a specific set of pages + */ + 'query' => function (string $query = null) { + return $query; + }, + + /** + * Layout size for cards + */ + 'size' => function (string $size = null) { + return $size; + }, + + /** + * Main text + */ + 'text' => function (string $text = null) { + return $text; + }, + + 'value' => function ($value = null) { + return $this->toPages($value); + }, + ], + 'methods' => [ + 'pageResponse' => function ($page) { + if ($this->layout === 'list') { + $thumb = [ + 'width' => 100, + 'height' => 100 + ]; + } else { + $thumb = [ + 'width' => 400, + 'height' => 400 + ]; + } + + $image = $page->panelImage($this->image, $thumb); + $model = $this->model(); + + return [ + 'text' => $page->toString($this->text ?? '{{ page.title }}'), + 'link' => $page->panelUrl(true), + 'id' => $page->id(), + 'info' => $page->toString($this->info ?? false), + 'image' => $image, + 'icon' => $page->panelIcon($image), + 'hasChildren' => $page->hasChildren(), + ]; + }, + '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(); + $query = $field->query(); + + if ($query) { + $pages = $field->model()->query($query, 'Kirby\Cms\Pages'); + $model = null; + } else { + if (!$parent = $this->site()->find($this->requestQuery('parent'))) { + $parent = $this->site(); + } + + $pages = $parent->children(); + $model = [ + 'id' => $parent->id() == '' ? null : $parent->id(), + 'title' => $parent->title()->value() + ]; + } + + $children = []; + + foreach ($pages as $index => $page) { + if ($page->isReadable() === true) { + $children[] = $field->pageResponse($page); + } + } + + return [ + 'model' => $model, + 'pages' => $children + ]; + } + ] + ]; + }, + '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..750e2ad --- /dev/null +++ b/kirby/config/fields/radio.php @@ -0,0 +1,32 @@ + ['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' => [ + 'options' => function (): array { + return $this->getOptions(); + }, + '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..b6bfb4f --- /dev/null +++ b/kirby/config/fields/select.php @@ -0,0 +1,18 @@ + 'radio', + 'props' => [ + /** + * Unset inherited props + */ + 'columns' => null, + + /** + * Custom icon to replace the arrow down. + */ + 'icon' => function (string $icon = null) { + return $icon; + }, + ] +]; diff --git a/kirby/config/fields/structure.php b/kirby/config/fields/structure.php new file mode 100755 index 0000000..e3d940c --- /dev/null +++ b/kirby/config/fields/structure.php @@ -0,0 +1,159 @@ + [ + /** + * 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); + }, + /** + * 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 () { + 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 () { + $data = []; + + foreach ($this->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..87b6ba8 --- /dev/null +++ b/kirby/config/fields/tags.php @@ -0,0 +1,91 @@ + ['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' => [ + 'options' => function () { + return $this->getOptions(); + }, + 'default' => function (): array { + return $this->toTags($this->default); + }, + 'value' => function (): array { + return $this->toTags($this->value); + } + ], + 'methods' => [ + 'toTags' => function ($value) { + $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)); + } + ], + '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..1217af9 --- /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' + ] +]; diff --git a/kirby/config/fields/textarea.php b/kirby/config/fields/textarea.php new file mode 100755 index 0000000..a724816 --- /dev/null +++ b/kirby/config/fields/textarea.php @@ -0,0 +1,61 @@ + [ + /** + * 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, list, code, ul, ol + */ + '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); + }, + + /** + * 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; + }, + + 'value' => function (string $value = null) { + return trim($value); + } + ], + 'validations' => [ + 'minlength', + 'maxlength' + ] +]; diff --git a/kirby/config/fields/time.php b/kirby/config/fields/time.php new file mode 100755 index 0000000..5b9d5f4 --- /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..6182788 --- /dev/null +++ b/kirby/config/fields/toggle.php @@ -0,0 +1,64 @@ + [ + /** + * Unset inherited props + */ + 'placeholder' => null, + + /** + * Default value which will be saved when a new Page/User/File is created + */ + 'default' => function ($value = null) { + return $this->toBool($value); + }, + /** + * 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' => [ + '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..6aaa5f2 --- /dev/null +++ b/kirby/config/fields/url.php @@ -0,0 +1,39 @@ + '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..0db4c44 --- /dev/null +++ b/kirby/config/fields/users.php @@ -0,0 +1,95 @@ + [ + /** + * 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); + }, + /** + * The minimum number of required selected users + */ + 'min' => function (int $min = null) { + return $min; + }, + /** + * The maximum number of allowed selected users + */ + 'max' => function (int $max = null) { + return $max; + }, + /** + * If false, only a single user can be selected + */ + 'multiple' => function (bool $multiple = true) { + return $multiple; + }, + 'value' => function ($value = null) { + return $this->toUsers($value); + }, + ], + 'methods' => [ + 'userResponse' => function ($user) { + $avatar = function ($user) { + if ($avatar = $user->avatar()) { + return [ + 'url' => $avatar->crop(512)->url() + ]; + } + + return null; + }; + + return [ + 'username' => $user->username(), + 'id' => $user->id(), + 'email' => $user->email(), + 'avatar' => $avatar($user) + ]; + }, + '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; + } + ], + 'save' => function ($value = null) { + return A::pluck($value, 'email'); + }, + 'validations' => [ + 'max', + 'min' + ] +]; diff --git a/kirby/config/helpers.php b/kirby/config/helpers.php new file mode 100755 index 0000000..dfd9cb5 --- /dev/null +++ b/kirby/config/helpers.php @@ -0,0 +1,764 @@ +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|boolean 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) +{ + if (is_array($url) === true) { + $links = array_map(function ($url) use ($options) { + return css($url, $options); + }, $url); + + return implode(PHP_EOL, $links); + } + + $href = $url === '@auto' ? Url::toTemplateAsset('css/templates', 'css') : Url::to($url); + + $attr = [ + 'href' => $href, + 'rel' => 'stylesheet' + ]; + + if (is_string($options) === true) { + $attr['media'] = $options; + } + + if (is_array($options) === true) { + $attr = array_merge($options, $attr); + } + + return ''; +} + +/** + * Simple object and variable dumper + * to help with debugging. + * + * @param mixed $variable + * @param boolean $echo + * @return string + */ +function dump($variable, bool $echo = true): string +{ + if (Server::cli() === true) { + $output = print_r($variable, true) . PHP_EOL; + } else { + $output = '
' . print_r($variable, true) . '
'; + } + + if ($echo === true) { + echo $output; + } + + return $output; +} + +/** + * 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 boolean $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 integer $code + * @return void + */ +function go(string $url = null, int $code = 302) +{ + die(Response::redirect($url, $code)); +} + +/** + * Shortcut for html() + * + * @param string $text 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 $text 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 File|null + */ +function image(string $path = null) +{ + if ($path === null) { + return page()->image(); + } + + $uri = dirname($path); + $filename = basename($path); + + if ($uri === '.') { + $uri = null; + } + + $page = $uri === '/' ? site() : page($uri); + + if ($page) { + return $page->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 $src + * @param string|array $options + * @return void + */ +function js($url, $options = null) +{ + if (is_array($url) === true) { + $scripts = array_map(function ($url) use ($options) { + return js($url, $options); + }, $url); + + return implode(PHP_EOL, $scripts); + } + + $src = $url === '@auto' ? Url::toTemplateAsset('js/templates', 'js') : Url::to($url); + $attr = [ + 'src' => $src, + ]; + + if (is_bool($options) === true) { + $attr['async'] = $options; + } + + if (is_array($options) === true) { + $attr = array_merge($options, $attr); + } + + return ''; +} + +/** + * Returns the Kirby object in any situation + * + * @return App + */ +function kirby(): App +{ + 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); +} + +/** + * A super simple class autoloader + * + * @param array $classmap + * @param string $base + * @return void + */ +function load(array $classmap, string $base = null) +{ + 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 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 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 integer $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 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 $name + * @param array|object $data + * @param boolean $return + * @return string + */ +function snippet(string $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 $file + * @return string + */ +function svg(string $file) +{ + $root = App::instance()->root(); + $file = $root . '/' . $file; + + if (file_exists($file) === false) { + return false; + } + + ob_start(); + include F::realpath($file, $root); + $svg = ob_get_contents(); + ob_end_clean(); + + return $svg; +} + +/** + * 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); +} + +/** + * 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|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 $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::video($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::video($url, $options, $attr); +} diff --git a/kirby/config/methods.php b/kirby/config/methods.php new file mode 100755 index 0000000..0770619 --- /dev/null +++ b/kirby/config/methods.php @@ -0,0 +1,389 @@ + function (Field $field): bool { + return $field->toBool() === false; + }, + + /** + * Converts the field value into a proper boolean + * + * @param Field $field + * @return boolean + */ + '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 boolean + */ + 'isValid' => function (Field $field, string $validator, ...$arguments): bool { + return V::$validator($field->value, ...$arguments); + }, + + // converters + + /** + * Parses the field value with the given method + * + * @param 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 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 Field $field + * @param string $format PHP date formatting string + * @return string|int + */ + 'toDate' => function (Field $field, string $format = null) use ($app) { + if (empty($field->value) === true) { + return null; + } + + if ($format === null) { + return $field->toTimestamp(); + } + + return $app->option('date.handler', 'date')($format, $field->toTimestamp()); + }, + + /** + * Returns a file object from a filename in the field + * + * @param Field $field + * @return File|null + */ + 'toFile' => function (Field $field) { + return $field->toFiles()->first(); + }, + + /** + * Returns a file collection from a yaml list of filenames in the field + * + * @return Files + */ + 'toFiles' => function (Field $field) { + return $field->parent()->files()->find(false, false, ...$field->toData('yaml')); + }, + + /** + * Converts the field value into a proper float + * + * @param 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 floatval($value); + }, + + /** + * Converts the field value into a proper integer + * + * @param 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 intval($value); + }, + + /** + * Wraps a link tag around the field value. The field value is used as the link text + * + * @param 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 Field $field + * @return Page|null + */ + 'toPage' => function (Field $field) use ($app) { + return $field->toPages()->first(); + }, + + /** + * Returns a pages collection from a yaml list of page ids in the field + * + * @param string $separator Can be any other separator to split the field value by + * @return 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 + */ + 'toStructure' => function (Field $field) { + return new Structure(Yaml::decode($field->value), $field->parent()); + }, + + /** + * Converts the field value to a Unix timestamp + */ + 'toTimestamp' => function (Field $field) { + return strtotime($field->value); + }, + + /** + * Turns the field value into an absolute Url + */ + 'toUrl' => function (Field $field) { + return Url::to($field->value); + }, + + /** + * Converts a user email address to a user object + * + * @return User|null + */ + 'toUser' => function (Field $field) use ($app) { + return $field->toUsers()->first(); + }, + + /** + * Returns a users collection from a yaml list of user email addresses in the field + * + * @return Users + */ + 'toUsers' => function (Field $field) use ($app) { + return $app->users()->find(false, false, ...$field->toData('yaml')); + }, + + // 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 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. + */ + '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 + */ + 'html' => function (Field $field) { + $field->value = htmlentities($field->value, ENT_COMPAT, 'utf-8'); + return $field; + }, + + /** + * Converts the field content from Markdown/Kirbytext to valid HTML + */ + 'kirbytext' => function (Field $field) use ($app) { + $field->value = $app->kirbytext($field->value, [ + 'parent' => $field->parent(), + 'field' => $field + ]); + + return $field; + }, + + /** + * Parses all KirbyTags without also parsing Markdown + */ + 'kirbytags' => function (Field $field) use ($app) { + $field->value = $app->kirbytags($field->value, [ + 'parent' => $field->parent(), + 'field' => $field + ]); + + return $field; + }, + + /** + * Converts the field content to lowercase + */ + 'lower' => function (Field $field) { + $field->value = Str::lower($field->value); + return $field; + }, + + /** + * Converts markdown to valid HTML + */ + 'markdown' => function (Field $field) use ($app) { + $field->value = $app->markdown($field->value); + return $field; + }, + + /** + * Converts the field content to valid XML + */ + '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 int $length The number of characters in the string + * @param string $appendix An optional replacement for the missing rest + * @return 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 + */ + 'slug' => function (Field $field) { + $field->value = Str::slug($field->value); + return $field; + }, + + /** + * Applies SmartyPants to the field + */ + 'smartypants' => function (Field $field) use ($app) { + $field->value = $app->smartypants($field->value); + return $field; + }, + + /** + * Splits the field content into an array + */ + 'split' => function (Field $field, $separator = ',') { + return Str::split((string)$field->value, $separator); + }, + + /** + * Converts the field content to uppercase + */ + 'upper' => function (Field $field) { + $field->value = Str::upper($field->value); + return $field; + }, + + /** + * Avoids typographical widows in strings by replacing the last space with   + */ + 'widont' => function (Field $field) { + $field->value = Str::widont($field->value); + return $field; + }, + + // aliases + + /** + * Parses yaml in the field content and returns an 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..576981e --- /dev/null +++ b/kirby/config/presets/files.php @@ -0,0 +1,14 @@ + [ + 'headline' => $props['headline'] ?? t('files'), + 'type' => 'files', + 'layout' => $props['layout'] ?? 'cards', + 'info' => '{{ file.dimensions }}' + ] + ]; + + 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..710a49d --- /dev/null +++ b/kirby/config/roots.php @@ -0,0 +1,85 @@ + function (array $roots) { + return realpath(__DIR__ . '/../'); + }, + 'translations' => function (array $roots) { + return $roots['kirby'] . '/translations'; + }, + + // 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'; + }, + 'emails' => function (array $roots) { + return $roots['site'] . '/emails'; + }, + '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..b7d2c9e --- /dev/null +++ b/kirby/config/routes.php @@ -0,0 +1,200 @@ +option('api.slug', 'api'); + $panel = $kirby->option('panel.slug', 'panel'); + + /** + * 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 $extension) use ($kirby) { + return $kirby + ->response() + ->type($extension) + ->body(PluginAssets::index($extension)); + } + ], + [ + '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) use ($kirby) { + if ($url = PluginAssets::resolve($provider . '/' . $pluginName, $filename . '.' . $extension)) { + return $kirby + ->response() + ->redirect($url, 307); + } + } + ], + [ + '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); + } + ] + ]; + + // Multi-language setup + if ($kirby->multilang() === true) { + + // Multi-language home + $after[] = [ + 'pattern' => '', + 'method' => 'ALL', + 'env' => 'site', + 'action' => function () use ($kirby) { + $home = $kirby->site()->homePage(); + + if ($kirby->url() !== $home->url()) { + if ($kirby->option('languages.detect') === true) { + return $kirby + ->response() + ->redirect($kirby->detectedLanguage()->url()); + } else { + return $kirby + ->response() + ->redirect($kirby->site()->url()); + } + } else { + return $home; + } + } + ]; + + foreach ($kirby->languages() as $language) { + $after[] = [ + 'pattern' => trim($language->pattern() . '/(:all?)', '/'), + 'method' => 'ALL', + 'env' => 'site', + 'action' => function ($path = null) use ($kirby, $language) { + return $kirby->resolve($path, $language->code()); + } + ]; + } + + // fallback route for unprefixed default language URLs. + $after[] = [ + 'pattern' => '(:all)', + 'method' => 'ALL', + 'env' => 'site', + 'action' => function (string $path) use ($kirby) { + if ($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->resolve($path, $kirby->defaultLanguage()->code()); + } + } + ]; + } else { + + // Single-language home + $after[] = [ + 'pattern' => '', + 'method' => 'ALL', + 'env' => 'site', + 'action' => function () use ($kirby) { + return $kirby->site()->homePage(); + } + ]; + + // 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..97c2386 --- /dev/null +++ b/kirby/config/sections/files.php @@ -0,0 +1,229 @@ + [ + 'empty', + 'headline', + 'layout', + 'min', + 'max', + 'pagination', + 'parent', + ], + 'props' => [ + /** + * 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 + */ + '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; + }, + 'dragTextType' => function () { + return (option('panel')['kirbytext'] ?? true) ? 'kirbytext' : 'markdown'; + }, + 'parent' => function () { + return $this->parentModel(); + }, + 'files' => function () { + $files = $this->parent->files()->template($this->template); + + if ($this->sortBy) { + $files = $files->sortBy(...Str::split($this->sortBy, ' ')); + } elseif ($this->sortable === true) { + $files = $files->sortBy('sort', 'asc'); + } + + // apply the default pagination + $files = $files->paginate([ + 'page' => $this->page, + 'limit' => $this->limit + ]); + + return $files; + }, + 'data' => function () { + $data = []; + + if ($this->layout === 'list') { + $thumb = [ + 'width' => 100, + 'height' => 100 + ]; + } else { + $thumb = [ + 'width' => 400, + 'height' => 400 + ]; + } + + foreach ($this->files as $file) { + $image = $file->panelImage($this->image, $thumb); + + $data[] = [ + 'dragText' => $file->dragText($this->dragTextType), + 'filename' => $file->filename(), + 'id' => $file->id(), + 'text' => $file->toString($this->text), + 'info' => $file->toString($this->info ?? false), + 'icon' => $file->panelIcon($image), + 'image' => $image, + 'link' => $file->panelUrl(true), + 'parent' => $file->parent()->panelPath(), + '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; + } + + 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, + '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..8310bdc --- /dev/null +++ b/kirby/config/sections/info.php @@ -0,0 +1,36 @@ + [ + '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..4632ce0 --- /dev/null +++ b/kirby/config/sections/mixins/empty.php @@ -0,0 +1,28 @@ + [ + /** + * Sets the text for the empty state box + */ + 'empty' => function (string $empty = null) { + return I18n::translate($empty); + } + ], + 'methods' => [ + 'isFull' => function () { + if ($this->max) { + return $this->total >= $this->max; + } + + return false; + }, + 'validateMax' => function () { + if ($this->max && $this->max < $this->total) { + return false; + } + + return true; + } + ] +]; diff --git a/kirby/config/sections/mixins/headline.php b/kirby/config/sections/mixins/headline.php new file mode 100755 index 0000000..80594cc --- /dev/null +++ b/kirby/config/sections/mixins/headline.php @@ -0,0 +1,19 @@ + [ + /** + * 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 () { + return $this->headline ?? ucfirst($this->name); + } + ] +]; diff --git a/kirby/config/sections/mixins/layout.php b/kirby/config/sections/mixins/layout.php new file mode 100755 index 0000000..7ab1634 --- /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..7a7c535 --- /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->max < $this->total) { + 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..2db9300 --- /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 $page ?? get('page', 1); + }, + ], + '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..e441b1a --- /dev/null +++ b/kirby/config/sections/mixins/parent.php @@ -0,0 +1,29 @@ + [ + /** + * 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) { + $parent = $this->model->query($parent); + } + + 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..1fc13ac --- /dev/null +++ b/kirby/config/sections/pages.php @@ -0,0 +1,290 @@ + [ + 'empty', + 'headline', + 'layout', + 'min', + 'max', + 'pagination', + 'parent' + ], + 'props' => [ + /** + * Optional array of templates that should only be allowed to add. + */ + 'create' => function ($add = null) { + return A::wrap($add); + }, + /** + * 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 + */ + '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; + }, + /** + * 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' => [ + 'dragTextType' => function () { + return option('panel.kirbytext', true) ? 'kirbytext' : 'markdown'; + }, + 'templates' => function () { + return A::wrap($this->templates ?? $this->template); + }, + '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(...Str::split($this->sortBy, ' ')); + } + + // pagination + $pages = $pages->paginate([ + 'page' => $this->page, + 'limit' => $this->limit + ]); + + return $pages; + }, + 'total' => function () { + return $this->pages->pagination()->total(); + }, + 'data' => function () { + $data = []; + + if ($this->layout === 'list') { + $thumb = [ + 'width' => 100, + 'height' => 100 + ]; + } else { + $thumb = [ + 'width' => 400, + 'height' => 400 + ]; + } + + foreach ($this->pages as $item) { + $permissions = $item->permissions(); + $blueprint = $item->blueprint(); + $image = $item->panelImage($this->image, $thumb); + + $data[] = [ + 'id' => $item->id(), + 'dragText' => $item->dragText($this->dragTextType), + '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->max), [ + 'min' => $this->min, + 'section' => $this->headline + ]); + } + + if (empty($errors) === true) { + return []; + } + + return [ + $this->name => [ + 'label' => $this->headline, + 'message' => $errors, + ] + ]; + }, + 'add' => function () { + 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 ($this->status !== 'listed' && $this->status !== 'all') { + return false; + } + + if ($this->sortable === false) { + return false; + } + + if ($this->sortBy !== null) { + return false; + } + + return true; + } + ], + 'methods' => [ + 'blueprints' => function () { + $blueprints = []; + $templates = empty($this->create) === false ? $this->create : $this->templates; + + if (empty($templates) === true) { + foreach (glob(App::instance()->root('blueprints') . '/pages/*.yml') as $blueprint) { + $templates[] = F::name($blueprint); + } + } + + // 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, + 'layout' => $this->layout, + 'link' => $this->link, + 'size' => $this->size, + 'sortable' => $this->sortable + ], + 'pagination' => $this->pagination, + ]; + } +]; diff --git a/kirby/config/tags.php b/kirby/config/tags.php new file mode 100755 index 0000000..2afde13 --- /dev/null +++ b/kirby/config/tags.php @@ -0,0 +1,225 @@ + [ + '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', + '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' => true, + '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', + 'text', + '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; + } + + return Html::a($tag->link === 'self' ? $tag->src : $tag->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); + } + + return Html::figure([ $link($image) ], $tag->caption, [ + 'class' => $tag->class + ]); + } + ], + + /* Link */ + 'link' => [ + 'attr' => [ + 'class', + 'rel', + 'role', + 'target', + 'title', + 'text', + ], + 'html' => function ($tag) { + 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', []) + ); + + return Html::figure([$video], $tag->caption, [ + 'class' => $tag->class ?? $tag->kirby()->option('kirbytext.video.class', 'video'), + 'height' => $tag->height ?? $tag->kirby()->option('kirbytext.video.height'), + 'width' => $tag->width ?? $tag->kirby()->option('kirbytext.video.width'), + ]); + } + ], + +]; diff --git a/kirby/config/tests.php b/kirby/config/tests.php new file mode 100755 index 0000000..0ea8692 --- /dev/null +++ b/kirby/config/tests.php @@ -0,0 +1,15 @@ + 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..25bdb62 --- /dev/null +++ b/kirby/dependencies/parsedown/Parsedown.php @@ -0,0 +1,1808 @@ +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 !== '') { + $Element['attributes'] = array('class' => "language-$infostring"); + } + + $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/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..e4f88da --- /dev/null +++ b/kirby/panel/dist/css/app.css @@ -0,0 +1 @@ +.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;display:-webkit-box;display:-ms-flexbox;display:flex}.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-input .k-button{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}*,:after,:before{margin:0;padding:0;-webkit-box-sizing:border-box;box-sizing:border-box}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{bottom:0;background:#efefef}.k-panel,.k-panel-header{position:absolute;top:0;right:0;left:0}.k-panel-header{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-loading]{-webkit-animation:Loading .5s;animation:Loading .5s}.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 Loading{to{cursor:progress}}@keyframes Loading{to{cursor:progress}}.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-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}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[data-disabled],.k-button[disabled]{pointer-events:none;opacity:.5}.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-figure{display:inline-block;line-height:0}.k-button-figure .k-icon{position:relative;top:0;color:currentColor}.k-button-figure img{width:16px;height:16px;background:#16171a;-o-object-fit:cover;object-fit:cover;border-radius:50%}[dir=ltr] .k-button-figure~.k-button-text{padding-left:.5rem}[dir=rtl] .k-button-figure~.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-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-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-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;color:hsla(0,0%,100%,.5)}.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-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-counter{font-size:.75rem;color:#16171a;font-weight:600}.k-counter[data-invalid]{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-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-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}[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-empty{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;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]{height:38px}.k-empty[data-layout=list] .k-icon{width:36px;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-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{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.k-tab-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 0;font-size:.75rem;text-transform:uppercase;font-weight:500;border-left:1px solid transparent;border-right:1px solid #ccc;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}@media screen and (min-width:30em){.k-tab-button{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}}@media screen and (min-width:65em){.k-tab-button{max-width:13rem}}@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;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}.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-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}.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}.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}.k-image[data-back=white] .k-image-error{background:#16171a}.k-image[data-back=pattern] span{background:#2d2f36 url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXR0ZXJuIGlkPSJhIiB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHBhdHRlcm5Vbml0cz0idXNlclNwYWNlT25Vc2UiPjxwYXRoIGZpbGw9InJnYmEoMCwgMCwgMCwgMC4yKSIgZD0iTTAgMGgxMHYxMEgwem0xMCAxMGgxMHYxMEgxMHoiLz48L3BhdHRlcm4+PHJlY3QgZmlsbD0idXJsKCNhKSIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIvPjwvc3ZnPg==")}.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{color:hsla(0,0%,100%,.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-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-icon{height:38px}.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{padding:1rem;font-size:.875rem}.k-pagination[data-align=center]{text-align:center}.k-pagination[data-align=right]{text-align:right}.k-pagination-selector{width:100%;padding:0!important}[dir=ltr] .k-pagination-selector{direction:ltr}[dir=rtl] .k-pagination-selector{direction:rtl}.k-pagination-selector>div{font-size:.875rem;display:-webkit-box;display:-ms-flexbox;display:flex}.k-pagination-selector>div>label{border-right:1px solid hsla(0,0%,100%,.1)}.k-pagination-selector>div>input,.k-pagination-selector>div>label{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;padding:.5rem 1rem}.k-pagination-selector>div>input{font:inherit;border:0;background:#4271ae;color:#16171a;border-top-right-radius:1px;border-bottom-right-radius:1px}.k-pagination-selector>div>input:focus{outline:0}.k-prev-next{direction:ltr}.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:-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;will-change:opacity,color;-webkit-transition:opacity .3s;transition:opacity .3s;z-index:1}.k-sort-handle .k-icon{width:100%;height:100%}.k-sort-handle:active{cursor:-webkit-grabbing}.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-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],.k-text[data-align=right]{text-align:center}.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.5rem}.k-view{padding-left:1.5rem;padding-right:1.5rem;margin:0 auto;max-width:100rem}@media screen and (min-width:30em){.k-view{padding-left:3rem;padding-right:3rem}}@media screen and (min-width:90em){.k-view{padding-left:6rem;padding-right:6rem}}.k-view[data-align=center]{height:100vh;display:-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-form-submitter{display:none}.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{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.k-field[data-disabled]{cursor:not-allowed}.k-field[data-disabled] *{pointer-events:none}.k-field:focus-within>.k-field-header>.k-field-counter{display:block}.k-field-help{padding-top:.5rem}.k-fieldset{border:0}.k-fieldset .k-grid{grid-row-gap:2.25rem}@media screen and (min-width:30em){.k-fieldset .k-grid{grid-column-gap:1.5rem}}.k-sections>.k-column[data-width="1/3"] .k-fieldset .k-grid,.k-sections>.k-column[data-width="1/4"] .k-fieldset .k-grid{grid-template-columns:repeat(1,1fr)}.k-sections>.k-column[data-width="1/3"] .k-fieldset .k-grid .k-column,.k-sections>.k-column[data-width="1/4"] .k-fieldset .k-grid .k-column{grid-column-start: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][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)}.k-input[data-theme=field][data-invalid]:focus-within{border:1px solid #c82829;-webkit-box-shadow:rgba(200,40,41,.25) 0 0 0 2px;box-shadow:0 0 0 2px rgba(200,40,41,.25)}.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::-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::-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-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-toolbar{background:#fff;border-bottom:1px solid #efefef;height:38px}.k-toolbar-buttons{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.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-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::-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-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: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}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-structure-table{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{font-weight:400;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-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="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:-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-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-url-field-preview a{color:#4271ae;text-decoration:underline;-webkit-transition:color .3s;transition:color .3s;display:inline-block;padding:0 .75rem;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-users-field-preview{padding:0 .25rem 0 .75rem;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-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-pages-dialog .k-empty{border:0}.k-users-dialog .k-list-item{cursor:pointer}.k-users-dialog .k-empty{border:0}.k-form-buttons{background:#de935f}.k-form-buttons .k-view{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.k-form-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{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-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-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-image img{padding:3rem}.k-file-preview-image-link{display:block;outline:0}.k-file-preview-image-link[data-tabbed]{outline:2px solid #4271ae!important;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-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;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-left:-.75rem;margin-right:-.75rem}.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 .k-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;outline:none}.k-topbar-view-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)}[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:center;-ms-flex-align:center;align-items:center;z-index:1}.k-section-header .k-headline{line-height:1.25rem;padding-bottom:.75rem;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;min-height:2rem}.k-section-header .k-button-group{position:absolute;top:-.975rem}[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}.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-browser-view .k-error-view-content{text-align:left}.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-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-login-form[data-invalid]{-webkit-animation:shake .5s linear;animation:shake .5s linear}.k-login-form[data-invalid] .k-field label{-webkit-animation:nope 2s linear;animation:nope 2s linear}.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}@-webkit-keyframes nope{0%{color:#c82829}to{color:#16171a}}@keyframes nope{0%{color:#c82829}to{color:#16171a}}@-webkit-keyframes shake{8%,41%{-webkit-transform:translateX(-10px);transform:translateX(-10px)}25%,58%{-webkit-transform:translateX(10px);transform:translateX(10px)}75%{-webkit-transform:translateX(-5px);transform:translateX(-5px)}92%{-webkit-transform:translateX(5px);transform:translateX(5px)}0%,to{-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes shake{8%,41%{-webkit-transform:translateX(-10px);transform:translateX(-10px)}25%,58%{-webkit-transform:translateX(10px);transform:translateX(10px)}75%{-webkit-transform:translateX(-5px);transform:translateX(-5px)}92%{-webkit-transform:translateX(5px);transform:translateX(5px)}0%,to{-webkit-transform:translateX(0);transform:translateX(0)}}.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-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{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..2accf55 --- /dev/null +++ b/kirby/panel/dist/img/icons.svg @@ -0,0 +1,401 @@ + diff --git a/kirby/panel/dist/js/app.js b/kirby/panel/dist/js/app.js new file mode 100755 index 0000000..df271ca --- /dev/null +++ b/kirby/panel/dist/js/app.js @@ -0,0 +1 @@ +(function(t){function e(e){for(var i,a,r=e[0],l=e[1],u=e[2],p=0,d=[];p=0&&this.selected--}}},h=f,m=(n("4cb2"),n("2877")),g=Object(m["a"])(h,a,r,!1,null,null,null);g.options.__file="Search.vue";var v=g.exports,b=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",attrs:{button:t.$t("license.register"),size:"medium"},on:{submit:t.submit}},[n("k-form",{attrs:{fields:t.fields,novalidate:!0},on:{submit:t.submit},model:{value:t.registration,callback:function(e){t.registration=e},expression:"registration"}})],1)},k=[],_={methods:{open:function(){this.$refs.dialog.open(),this.$emit("open")},close:function(){this.$refs.dialog.close(),this.$emit("close")},success:function(t){this.$refs.dialog.close(),t.route&&this.$router.push(t.route),t.message&&this.$store.dispatch("notification/success",t.message),t.event&&this.$events.$emit(t.event),this.$emit("success")}}},$={mixins:[_],data:function(){return{registration:{license:null,email:null}}},computed:{fields:function(){return{license:{label:this.$t("license.register.label"),type:"text",required:!0,counter:!1,placeholder:"K3-",help:this.$t("license.register.help")},email:{label:this.$t("email"),type:"email",required:!0,counter:!1}}}},methods:{submit:function(){var t=this;this.$api.system.register(this.registration).then(function(){t.$store.dispatch("system/register",t.registration.license),t.success({message:t.$t("license.register.success")})}).catch(function(e){t.$refs.dialog.error(e.message)})}}},y=$,x=Object(m["a"])(y,b,k,!1,null,null,null);x.options.__file="RegistrationDialog.vue";var w=x.exports,S={name:"App",components:{"k-registration":w,"k-search":v},data:function(){return{offline:!1,dragging:!1,debug:d.debug}},computed:{inside:function(){return!(this.$route.meta.outside||!this.$store.state.user.current)}},created:function(){this.$events.$on("offline",this.isOffline),this.$events.$on("online",this.isOnline),this.$events.$on("keydown.cmd.shift.f",this.search),this.$events.$on("drop",this.drop)},destroyed:function(){this.$events.$off("offline",this.isOffline),this.$events.$off("online",this.isOnline),this.$events.$off("keydown.cmd.shift.f",this.search),this.$events.$off("drop",this.drop)},methods:{drop:function(){this.$store.dispatch("drag",null)},isOnline:function(){this.offline=!1},isOffline:function(){!1===this.$store.state.system.info.isLocal&&(this.offline=!0)},search:function(t){t.preventDefault(),this.$store.dispatch("search",!0)}}},O=S,C=(n("5c0b"),Object(m["a"])(O,s,o,!1,null,null,null));C.options.__file="App.vue";var E=C.exports,j=n("1dce"),T=n.n(j);n("6762"),n("2fdb");function I(t){var e=String(t);return e.charAt(0).toLowerCase()+e.substr(1)}var L={install:function(t){t.prototype.$events=new t({data:function(){return{entered:null}},created:function(){window.addEventListener("online",this.online),window.addEventListener("offline",this.offline),window.addEventListener("dragenter",this.dragenter,!1),window.addEventListener("dragover",this.prevent,!1),window.addEventListener("dragexit",this.prevent,!1),window.addEventListener("dragleave",this.dragleave,!1),window.addEventListener("drop",this.drop,!1),window.addEventListener("keydown",this.keydown,!1),window.addEventListener("keyup",this.keyup,!1),document.addEventListener("click",this.click,!1)},destroyed:function(){window.removeEventListener("online",this.online),window.removeEventListener("offline",this.offline),window.removeEventListener("dragenter",this.dragenter,!1),window.removeEventListener("dragover",this.prevent,!1),window.removeEventListener("dragexit",this.prevent,!1),window.removeEventListener("dragleave",this.dragleave,!1),window.removeEventListener("drop",this.drop,!1),window.removeEventListener("keydown",this.keydown,!1),window.removeEventListener("keyup",this.keyup,!1),document.removeEventListener("click",this.click,!1)},methods:{click:function(t){this.$emit("click",t)},drop:function(t){this.prevent(t),this.$emit("drop",t)},dragenter:function(t){this.entered=t.target,this.prevent(t),this.$emit("dragenter",t)},dragleave:function(t){this.prevent(t),this.entered===t.target&&this.$emit("dragleave",t)},keydown:function(t){var e=["keydown"];(t.metaKey||t.ctrlKey)&&e.push("cmd"),!0===t.altKey&&e.push("alt"),!0===t.shiftKey&&e.push("shift");var n=I(t.key),i={escape:"esc",arrowUp:"up",arrowDown:"down",arrowLeft:"left",arrowRight:"right"};i[n]&&(n=i[n]),!1===["alt","control","shift","meta"].includes(n)&&e.push(n),this.$emit(e.join("."),t),this.$emit("keydown",t)},keyup:function(t){this.$emit("keyup",t)},online:function(t){this.$emit("online",t)},offline:function(t){this.$emit("offline",t)},prevent:function(t){t.stopPropagation(),t.preventDefault()}}})}},A=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()])},q=[],N=(n("0dac"),{}),P=Object(m["a"])(N,A,q,!1,null,null,null);P.options.__file="Bar.vue";var D=P.exports,B=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)},F=[],R={props:{theme:String,text:String}},M=R,z=(n("3460"),Object(m["a"])(M,B,F,!1,null,null,null));z.options.__file="Box.vue";var U=z.exports,H=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n(t.component,t._g({directives:[{name:"tab",rawName:"v-tab"}],ref:"button",tag:"component",staticClass:"k-button",attrs:{"aria-current":t.current,autofocus:t.autofocus,id:t.id,disabled:t.disabled,"data-tabbed":t.tabbed,"data-theme":t.theme,"data-responsive":t.responsive,role:t.role,tabindex:t.tabindex,target:t.target,title:t.tooltip,to:t.link,type:t.link?null:t.type}},t.$listeners),[t.image||t.icon?n("figure",{staticClass:"k-button-figure"},[t.image?n("img",{attrs:{src:t.imageUrl,alt:t.tooltip||""}}):n("k-icon",{attrs:{type:t.icon,alt:t.tooltip}})],1):t._e(),t.$slots.default?n("span",{staticClass:"k-button-text"},[t._t("default")],2):t._e()])},V=[],K=n("53ca"),G=(n("c5f6"),{inheritAttrs:!1,props:{autofocus:Boolean,current:[String,Boolean],disabled:Boolean,icon:String,id:[String,Number],image:[String,Object],link:String,responsive:Boolean,role:String,target:String,tabindex:String,theme:String,tooltip:String,type:{type:String,default:"button"}},data:function(){return{tabbed:!1}},computed:{component:function(){return this.link?"k-link":"button"},imageUrl:function(){return this.image?"object"===Object(K["a"])(this.image)?this.image.url:this.image:null}},methods:{focus:function(){this.$refs.button.focus()},tab:function(){this.focus(),this.tabbed=!0},untab:function(){this.tabbed=!1}}}),Y=G,W=(n("bd6e"),Object(m["a"])(Y,H,V,!1,null,null,null));W.options.__file="Button.vue";var J=W.exports,X=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-button-group"},[t._t("default")],2)},Q=[],Z=(n("2c67"),{}),tt=Object(m["a"])(Z,X,Q,!1,null,null,null);tt.options.__file="ButtonGroup.vue";var et=tt.exports,nt=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,empty:!1,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,empty:!1,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){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:function(e){t.go("today")}}},[t._v(t._s(t.$t("today")))])],1)])])])])},it=[],st=(n("ac6a"),n("5a0c")),ot=n.n(st),at=function(t,e){t=String(t);var n="";e=(e||2)-t.length;while(n.length0?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:at(e)});return t}},watch:{value:function(t){var e=ot()(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()},select:function(t){t&&(this.day=t);var e=ot()(new Date(this.year,this.month,this.day,this.current.hour(),this.current.minute()));this.$emit("input",e.toISOString())}}},lt=rt,ut=(n("4c3f"),Object(m["a"])(lt,nt,it,!1,null,null,null));ut.options.__file="Calendar.vue";var ct=ut.exports,pt=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.image&&t.image.url?n("k-image",{staticClass:"k-card-image",attrs:{src:t.image.url,ratio:t.image.ratio||"3/2",back:t.image.back||"black",cover:t.image.cover}}):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){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){t.$emit("action",e)}}})])],2)],1)},dt=[],ft=(n("28a5"),function(t){t=t||"3/2";var e=t.split("/");if(2!==e.length)return"100%";var n=Number(e[0]),i=Number(e[1]),s=100/n*i;return s+"%"}),ht={inheritAttrs:!1,props:{flag:Object,icon:{type:Object,default:function(){return{type:"file",back:"black"}}},image:Object,info:String,link:String,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?ft(this.icon.ratio):ft("3/2")}}},mt=ht,gt=(n("5369"),Object(m["a"])(mt,pt,dt,!1,null,null,null));gt.options.__file="Card.vue";var vt=gt.exports,bt=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)},kt=[],_t={props:{cards:Array}},$t=_t,yt=(n("2666"),Object(m["a"])($t,bt,kt,!1,null,null,null));yt.options.__file="Cards.vue";var xt=yt.exports,wt=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){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){t.$emit("action",e,n)},dragstart:function(n){t.onDragStart(n,e.dragText)}}},"component",e,!1))}),1),!1!==t.pagination&&!0!==t.paginationOptions.hide?n("k-pagination",t._b({on:{paginate:function(e){t.$emit("paginate",e)}}},"k-pagination",t.paginationOptions,!1)):t._e()],1)},St=[],Ot={props:{items:{type:[Array,Object],default:function(){return[]}},layout:{type:String,default:"list"},size:String,sortable:Boolean,pagination:{type:[Boolean,Object],default:function(){return!1}}},data:function(){return{list:this.items}},computed:{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(K["a"])(this.pagination)?{}:this.pagination;return Object(u["a"])({limit:10,align:"center",details:!0,keys:!1,total:0,hide:!1},t)}},watch:{items:function(){this.list=this.items},$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})}}},Ct=Ot,Et=Object(m["a"])(Ct,wt,St,!1,null,null,null);Et.options.__file="Collection.vue";var jt=Et.exports,Tt=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)},It=[],Lt={name:"KirbyColumn",props:{width:String}},At=Lt,qt=(n("5e3a"),Object(m["a"])(At,Tt,It,!1,null,null,null));qt.options.__file="Column.vue";var Nt=qt.exports,Pt=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()])},Dt=[],Bt={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))}}},Ft=Bt,Rt=(n("5f5b"),Object(m["a"])(Ft,Pt,Dt,!1,null,null,null));Rt.options.__file="Counter.vue";var Mt=Rt.exports,zt=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.isOpen?n("div",{staticClass:"k-dialog",on:{click:t.cancel}},[n("div",{staticClass:"k-dialog-box",attrs:{"data-size":t.size},on:{click:function(t){t.stopPropagation()}}},[t.notification?n("div",{staticClass:"k-dialog-notification",attrs:{"data-theme":t.notification.type}},[n("p",[t._v(t._s(t.notification.message))]),n("k-button",{attrs:{icon:"cancel"},on:{click:function(e){t.notification=null}}})],1):t._e(),n("div",{staticClass:"k-dialog-body"},[t._t("default")],2),n("footer",{staticClass:"k-dialog-footer"},[t._t("footer",[n("k-button-group",[n("k-button",{staticClass:"k-dialog-button-cancel",attrs:{icon:"cancel"},on:{click:t.cancel}},[t._v("\n "+t._s(t.$t("cancel"))+"\n ")]),n("k-button",{staticClass:"k-dialog-button-submit",attrs:{icon:t.icon,theme:t.theme},on:{click:t.submit}},[t._v("\n "+t._s(t.button||t.$t("confirm"))+"\n ")])],1)])],2)])]):t._e()},Ut=[],Ht={props:{button:{type:String,default:"Ok"},icon:{type:String,default:"check"},size:String,theme:String,visible:Boolean},data:function(){return{notification:null,isOpen:this.visible,scrollTop:0}},mounted:function(){!0===this.isOpen&&this.$emit("open")},methods:{storeScrollPosition:function(){var t=document.querySelector(".k-panel-view");t&&t.scrollTop?this.scrollTop=t.scrollTop:this.scrollTop=0},restoreScrollPosition:function(){var t=document.querySelector(".k-panel-view");t&&t.scrollTop&&(t.scrollTop=this.scrollTop)},open:function(){var t=this;this.storeScrollPosition(),this.$store.dispatch("dialog",!0),this.notification=null,this.isOpen=!0,this.$emit("open"),this.$events.$on("keydown.esc",this.close),this.$nextTick(function(){t.$el&&(t.focus(),document.body.addEventListener("focus",function(e){!1===t.$el.contains(e.target)&&t.focus()},!0))})},close:function(){this.notification=null,this.isOpen=!1,this.$emit("close"),this.$events.$off("keydown.esc",this.close),this.$store.dispatch("dialog",null),this.restoreScrollPosition()},cancel:function(){this.$emit("cancel"),this.close()},focus:function(){if(this.$el&&this.$el.querySelector){var t=this.$el.querySelector("[autofocus], [data-autofocus], input, textarea, select, .k-dialog-button-submit");if(t||(t=this.$el.querySelector(".k-dialog-button-cancel")),t)return void t.focus()}},error:function(t){this.notification={message:t,type:"error"}},submit:function(){this.$emit("submit")},success:function(t){this.notification={message:t,type:"success"}}}},Vt=Ht,Kt=(n("4752"),Object(m["a"])(Vt,zt,Ut,!1,null,null,null));Kt.options.__file="Dialog.vue";var Gt=Kt.exports,Yt=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("draggable",t._g({staticClass:"k-draggable",attrs:{element:t.element,list:t.list,options:t.dragOptions}},t.listeners),[t._t("default"),t._t("footer",null,{slot:"footer"})],2)},Wt=[],Jt=n("1516"),Xt=n.n(Jt),Qt={components:{draggable:Xt.a},props:{element:String,handle:[String,Boolean],list:[Array,Object],options:Object},data:function(){var t=this;return{listeners:Object(u["a"])({},this.$listeners,{start:function(){t.$store.dispatch("drag",{}),t.$listeners.start&&t.$listeners.start()},end:function(){t.$store.dispatch("drag",null),t.$listeners.end&&t.$listeners.end()}})}},computed:{dragOptions:function(){var t=!1;return t=!0===this.handle?".k-sort-handle":this.handle,Object(u["a"])({fallbackClass:"k-sortable-fallback",fallbackOnBody:!0,forceFallback:!0,ghostClass:"k-sortable-ghost",handle:t,scroll:document.querySelector(".k-panel-view")},this.options)}}},Zt=Qt,te=Object(m["a"])(Zt,Yt,Wt,!1,null,null,null);te.options.__file="Draggable.vue";var ee=te.exports,ne=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)},ie=[],se=(n("df30"),{}),oe=Object(m["a"])(se,ne,ie,!1,null,null,null);oe.options.__file="Dropdown.vue";var ae=oe.exports,re=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 n("k-dropdown-item",t._b({key:t._uid+"-item-"+i,ref:t._uid+"-item-"+i,refInFor:!0,on:{click:function(n){t.$emit("action",e.click)}}},"k-dropdown-item",e,!1),[t._v("\n "+t._s(e.text)+"\n ")])}))],2):t._e()},le=[],ue=null,ce={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):Array.isArray(this.options)&&t(this.options)},open:function(){var t=this;this.reset(),ue&&ue!==this&&ue.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"),ue=t})},reset:function(){this.current=-1,this.$events.$off("keydown",this.navigate),this.$events.$off("click",this.close)},close:function(){this.reset(),this.isOpen=ue=!1,this.$emit("close")},toggle:function(){this.isOpen?this.close():this.open()},focus:function(t){t=t||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(),this.current>0?(this.current--,this.focus(this.current)):(this.close(),this.$emit("leave",t.code));break;case"ArrowDown":t.preventDefault(),this.current1?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){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)},Fe=[],Re={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"}}}}},Me=Re,ze=(n("b42a"),Object(m["a"])(Me,Be,Fe,!1,null,null,null));ze.options.__file="Header.vue";var Ue=ze.exports,He=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)},Ve=[],Ke={props:{link:String,size:{type:String},tag:{type:String,default:"h2"},theme:{type:String}}},Ge=Ke,Ye=(n("b83b"),Object(m["a"])(Ge,He,Ve,!1,null,null,null));Ye.options.__file="Headline.vue";var We=Ye.exports,Je=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("span",{staticClass:"k-icon",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}})])])},Xe=[],Qe={props:{alt:String,color:String,back:String,emoji:Boolean,size:String,type:String}},Ze=Qe,tn=(n("4496"),Object(m["a"])(Ze,Je,Xe,!1,null,null,null));tn.options.__file="Icon.vue";var en=tn.exports,nn=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("figure",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},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),t.caption?n("figcaption",{domProps:{innerHTML:t._s(t.caption)}}):t._e()])},sn=[],on={props:{src:String,alt:String,ratio:String,back:String,caption:String,cover:Boolean},data:function(){return{loaded:{type:Boolean,default:!1},error:{type:Boolean,default:!1}}},computed:{ratioPadding:function(){return ft(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}},an=on,rn=(n("791b"),Object(m["a"])(an,nn,sn,!1,null,null,null));rn.options.__file="Image.vue";var ln=rn.exports,un=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:{disabled:t.disabled,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)},cn=[],pn={props:{disabled:Boolean,rel:String,tabindex:String,target:String,title:String,to:String},data:function(){return{relAttr:"_blank"===this.target?"noreferrer noopener":this.rel,listeners:Object(u["a"])({},this.$listeners,{click:this.onClick})}},computed:{href:function(){return void 0!==this.$route&&"/"===this.to[0]?(this.$router.options.url||"")+this.to: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;this.isRoutable(t)&&(t.preventDefault(),this.$router.push(this.to)),this.$emit("click",t)},focus:function(){this.$refs.link.focus()}}},dn=pn,fn=Object(m["a"])(dn,un,cn,!1,null,null,null);fn.options.__file="Link.vue";var hn=fn.exports,mn=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)},gn=[],vn={props:{items:Array}},bn=vn,kn=(n("8e2d"),Object(m["a"])(bn,mn,gn,!1,null,null,null));kn.options.__file="List.vue";var _n=kn.exports,$n=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",{directives:[{name:"tab",rawName:"v-tab"}],staticClass:"k-list-item-content",attrs:{to:t.link,target:t.target}},[n("figure",{staticClass:"k-list-item-image"},[t.image&&t.image.url?n("k-image",{attrs:{src:t.image.url,back:t.image.back||"pattern",cover:t.image.cover}}):n("k-icon",t._b({},"k-icon",t.icon,!1))],1),n("figcaption",{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("div",{staticClass:"k-list-item-options"},[t._t("options",[t.flag?n("k-button",t._b({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){e.stopPropagation(),t.$refs.options.toggle()}}}):t._e(),n("k-dropdown-content",{ref:"options",attrs:{options:t.options,align:"right"},on:{action:function(e){t.$emit("action",e)}}})])],2)],1)},yn=[],xn={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,flag:Object,options:[Array,Function]}},wn=xn,Sn=(n("6022"),Object(m["a"])(wn,$n,yn,!1,null,null,null));Sn.options.__file="ListItem.vue";var On=Sn.exports,Cn=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.show?n("k-button-group",{staticClass:"k-pagination",attrs:{"data-align":t.align}},[n("k-button",{attrs:{disabled:!t.hasPrev,tooltip:t.prevLabel,icon:"angle-left"},on:{click:t.prev}}),t.details?[t.dropdown?[n("k-dropdown",[n("k-button",{staticClass:"k-pagination-details",attrs:{disabled:!t.hasPages},on:{click:function(e){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",[n("label",{attrs:{for:"k-pagination-input"}},[t._v(t._s(t.pageLabel))]),n("input",{ref:"page",attrs:{id:"k-pagination-input",min:1,max:t.pages,type:"number"},domProps:{value:t.currentPage},on:{focus:function(t){t.target.select()},input:function(e){t.goTo(e.target.value)}}})])])],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(),n("k-button",{attrs:{disabled:!t.hasNext,tooltip:t.nextLabel,icon:"angle-right"},on:{click:t.next}})],2):t._e()},En=[],jn={props:{align:{type:String,default:"left"},details:{type:Boolean,default:!1},dropdown:{type:Boolean,default:!0},validate:{type:Function,default:function(){return Promise.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:"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=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.$emit("paginate",{page:parseInt(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}}}},Tn=jn,In=(n("3acb"),Object(m["a"])(Tn,Cn,En,!1,null,null,null));In.options.__file="Pagination.vue";var Ln=In.exports,An=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)},qn=[],Nn={props:{prev:{type:Object,default:function(){return{disabled:!0,link:"#"}}},next:{type:Object,default:function(){return{disabled:!0,link:"#"}}}}},Pn=Nn,Dn=(n("2607"),Object(m["a"])(Pn,An,qn,!1,null,null,null));Dn.options.__file="PrevNext.vue";var Bn=Dn.exports,Fn=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")])},Rn=[],Mn={props:{value:{type:Number,default:0}},data:function(){return{state:this.value}},methods:{set:function(t){this.state=t}}},zn=Mn,Un=(n("8be2"),Object(m["a"])(zn,Fn,Rn,!1,null,null,null));Un.options.__file="Progress.vue";var Hn=Un.exports,Vn=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("span",{staticClass:"k-sort-handle"},[n("k-icon",{attrs:{type:"sort"}})],1)},Kn=[],Gn=(n("aa8b"),{}),Yn=Object(m["a"])(Gn,Vn,Kn,!1,null,null,null);Yn.options.__file="SortHandle.vue";var Wn=Yn.exports,Jn=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"button"in e||!t._k(e.keyCode,"delete",[8,46],e.key,["Backspace","Delete","Del"])?(e.preventDefault(),t.remove(e)):null}}},[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()])},Xn=[],Qn={props:{removable:Boolean,size:String},methods:{remove:function(){this.removable&&this.$emit("remove")},focus:function(){this.$refs.button.focus()}}},Zn=Qn,ti=(n("a361"),Object(m["a"])(Zn,Jn,Xn,!1,null,null,null));ti.options.__file="Tag.vue";var ei=ti.exports,ni=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)},ii=[],si={props:{align:String,size:String,theme:String}},oi=si,ai=(n("dea4"),Object(m["a"])(oi,ni,ii,!1,null,null,null));ai.options.__file="Text.vue";var ri=ai.exports,li=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)},ui=[],ci={props:{align:String}},pi=ci,di=(n("4cc7"),Object(m["a"])(pi,li,ui,!1,null,null,null));di.options.__file="View.vue";var fi=di.exports,hi=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:{click:function(n){t.onSelect(e)},keydown:[function(n){if(!("button"in n)&&t._k(n.keyCode,"tab",9,n.key,"Tab"))return null;n.preventDefault(),t.onSelect(e)},function(n){if(!("button"in n)&&t._k(n.keyCode,"enter",13,n.key,"Enter"))return null;n.preventDefault(),t.onSelect(e)},function(e){return"button"in e||!t._k(e.keyCode,"left",37,e.key,["Left","ArrowLeft"])?"button"in e&&0!==e.button?null:(e.preventDefault(),t.close(e)):null},function(e){return"button"in e||!t._k(e.keyCode,"backspace",void 0,e.key,void 0)?(e.preventDefault(),t.close(e)):null},function(e){return"button"in e||!t._k(e.keyCode,"delete",[8,46],e.key,["Backspace","Delete","Del"])?(e.preventDefault(),t.close(e)):null}]}},"k-dropdown-item",e,!1),[t._v("\n "+t._s(e.text)+"\n ")])}),1),t._v("\n "+t._s(t.query)+"\n")],2)},mi=[],gi=(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)&&-1===this.skip.indexOf(t)){var n=new RegExp(t,"ig");this.matches=this.options.filter(function(t){return!!t.text&&(-1===e.skip.indexOf(t.text)&&null!==t.text.match(n))}).slice(0,this.limit),this.$emit("search",t,this.matches),this.$refs.dropdown.open()}}}}),vi=gi,bi=(n("3f08"),Object(m["a"])(vi,hi,mi,!1,null,null,null));bi.options.__file="Autocomplete.vue";var ki=bi.exports,_i=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)},$i=[],yi={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(u["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()}}},xi=yi,wi=(n("8633"),Object(m["a"])(xi,_i,$i,!1,null,null,null));wi.options.__file="Form.vue";var Si=wi.exports,Oi=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){t.$emit("focus",e)},focusout:function(e){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:"This field is 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)},Ci=[],Ei={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||" "}}},ji=Ei,Ti=(n("fa44"),Object(m["a"])(ji,Oi,Ci,!1,null,null,null));Ti.options.__file="Field.vue";var Ii=Ti.exports,Li=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?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){t.$emit("input",t.value,e,i)},focus:function(n){t.$emit("focus",n,e,i)},invalid:function(n,s){return t.onInvalid(n,s,e,i)},submit:function(n){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=[],qi=(n("456d"),n("7f7f"),{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=Object.keys(this.$refs)[0];this.focus(e)}},hasFieldType:function(t){return i["a"].options.components["k-"+t+"-field"]},hasField:function(t){return this.$refs[t]&&this.$refs[t][0]},onInvalid:function(t,e,n,i){this.errors[i]=e,this.$emit("invalid",this.errors)},hasErrors:function(){return Object.keys(this.errors).length}}}),Ni=qi,Pi=(n("f986"),Object(m["a"])(Ni,Li,Ai,!1,null,null,null));Pi.options.__file="Fieldset.vue";var Di=Pi.exports,Bi=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()])},Fi=[],Ri={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]}},data:function(){var t=this;return{isInvalid:this.invalid,listeners:Object(u["a"])({},this.$listeners,{invalid:function(e,n){t.isInvalid=e,t.$emit("invalid",e,n)}}),inputProps:Object(u["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.focus)this.$refs.input.focus();else{var e=this.$el.querySelector("input, select, textarea");e&&e.focus()}}}},Mi=Ri,zi=(n("a2a8"),Object(m["a"])(Mi,Bi,Fi,!1,null,null,null));zi.options.__file="Input.vue";var Ui=zi.exports,Hi=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}}),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){t.$refs.dialog.close()}}},[t._v("\n "+t._s(t.$t("confirm"))+"\n ")])],1)]:t._e()],2)],2)],1)},Vi=[],Ki=n("2909"),Gi=(n("f751"),function(t,e){var n={url:"/",field:"file",method:"POST",accept:"text",attributes:{},complete:function(){},error:function(){},success:function(){},progress:function(){}},i=Object.assign(n,e),s=new FormData;s.append(i.field,t),i.attributes&&Object.keys(i.attributes).forEach(function(t){s.append(t,i.attributes[t])});var o=new XMLHttpRequest,a=function(e){if(e.lengthComputable&&i.progress){var n=Math.max(0,Math.min(100,e.loaded/e.total*100));i.progress(o,t,Math.ceil(n))}};o.addEventListener("loadstart",a),o.addEventListener("progress",a),o.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(o,t,n):(i.success(o,t,n),i.progress(o,t,100))}),o.addEventListener("error",function(e){var n=JSON.parse(e.target.response);i.error(o,t,n),i.progress(o,t,100)}),o.open("POST",i.url,!0),i.headers&&Object.keys(i.headers).forEach(function(t){var e=i.headers[t];o.setRequestHeader(t,e)}),o.send(s)}),Yi={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=Object.assign({},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(Ki["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){Gi(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){e.complete(n)},error:function(t,n,i){e.errors.push({file:n,message:i.message}),e.complete(n,i.message)}})})},complete:function(t){var e=this;if(this.completed[t.name]=!0,Object.keys(this.completed).length==this.total){if(this.$refs.input.value="",this.errors.length>0)return this.$forceUpdate(),void this.$emit("error",this.files);setTimeout(function(){e.$refs.dialog.close(),e.$emit("success",e.files)},250)}}}},Wi=Yi,Ji=(n("4a37"),Object(m["a"])(Wi,Hi,Vi,!1,null,null,null));Ji.options.__file="Upload.vue";var Xi=Ji.exports,Qi=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("label",{staticClass:"k-checkbox-input"},[n("input",t._g({ref:"input",staticClass:"k-checkbox-input-native",attrs:{disabled:t.disabled,id:t.id,type:"checkbox"},domProps:{checked:t.value}},t.listeners)),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)}})])},Zi=[],ts=n("b5ae"),es={inheritAttrs:!1,props:{autofocus:Boolean,disabled:Boolean,id:[Number,String],label:String,required:Boolean,value:Boolean},data:function(){var t=this;return{listeners:Object(u["a"])({},this.$listeners,{change:function(e){return t.onChange(e.target.checked)}})}},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.$refs.input.focus()}},validations:function(){return{value:{required:!this.required||ts["required"]}}}},ns=es,is=(n("38ee"),Object(m["a"])(ns,Qi,Zi,!1,null,null,null));is.options.__file="CheckboxInput.vue";var ss=is.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){t.onInput(e.value,n)}}})],1)}),0)},as=[],rs={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,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 Array.isArray(t)?t:String(t).split(",")}},validations:function(){return{selected:{required:!this.required||ts["required"],min:!this.min||Object(ts["minLength"])(this.min),max:!this.max||Object(ts["maxLength"])(this.max)}}}},ls=rs,us=Object(m["a"])(ls,os,as,!1,null,null,null);us.options.__file="CheckboxesInput.vue";var cs=us.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:"––––",empty:"––––"},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,empty:"––",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:"––",empty:"––"},on:{input:t.setDay,invalid:t.onInvalid}})],1)},ds=[],fs={inheritAttrs:!1,props:{autofocus:Boolean,disabled:Boolean,id:[String,Number],max:String,min:String,required:Boolean,value:String},data:function(){return{date:ot()(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=ot()(t)}},methods:{calculate:function(t,e){var n={min:{run:"subtract",take:"startOf"},max:{run:"add",take:"endOf"}}[e],i=t?ot()(t):null;return i&&!1!==i.isValid()||(i=ot()()[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:at(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,parseInt(e)),"month"===t&&this.date.date()!==i&&(this.date=n.set("date",1).set("month",e).endOf("month")),this.onInput()},setInvalid:function(){this.date=ot()("invalid")},setInitialDate:function(t,e){var n=ot()();return this.date=ot()().set(t,parseInt(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)}}},hs=fs,ms=(n("196d"),Object(m["a"])(hs,ps,ds,!1,null,null,null));ms.options.__file="DateInput.vue";var gs=ms.exports,vs=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,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)},bs=[],ks={inheritAttrs:!1,props:Object(u["a"])({},gs.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=ot()(t);return e.isValid()?e.format("YYYY-MM-DD"):null},parseTime:function(t){var e=ot()(t);return e.isValid()?e.format("HH:mm"):null},setDate:function(t){t&&!this.timeValue&&(this.timeValue=ot()().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=ot()().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||ts["required"]}}}},_s=ks,$s=(n("988f"),Object(m["a"])(_s,vs,bs,!1,null,null,null));$s.options.__file="DateTimeInput.vue";var ys=$s.exports,xs=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))},ws=[],Ss={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(u["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(){return{value:{required:!this.required||ts["required"],minLength:!this.minlength||Object(ts["minLength"])(this.minlength),maxLength:!this.maxlength||Object(ts["maxLength"])(this.maxlength),email:"email"!==this.type||ts["email"],url:"url"!==this.type||ts["url"]}}}},Os=Ss,Cs=(n("1182"),Object(m["a"])(Os,xs,ws,!1,null,null,null));Cs.options.__file="TextInput.vue";var Es,js,Ts=Cs.exports,Is={extends:Ts,props:Object(u["a"])({},Ts.props,{autocomplete:{type:String,default:"email"},placeholder:{type:String,default:function(){return this.$t("email.placeholder")}},type:{type:String,default:"email"}})},Ls=Is,As=Object(m["a"])(Ls,Es,js,!1,null,null,null);As.options.__file="EmailInput.vue";var qs=As.exports,Ns=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){t.remove(e)}},nativeOn:{click:function(t){t.stopPropagation()},keydown:[function(e){return"button"in e||!t._k(e.keyCode,"left",37,e.key,["Left","ArrowLeft"])?"button"in e&&0!==e.button?null:void t.navigate("prev"):null},function(e){return"button"in e||!t._k(e.keyCode,"right",39,e.key,["Right","ArrowRight"])?"button"in e&&2!==e.button?null:void t.navigate("next"):null},function(e){return"button"in e||!t._k(e.keyCode,"down",40,e.key,["Down","ArrowDown"])?t.$refs.dropdown.open(e):null}]}},[t._v("\n "+t._s(e.text)+"\n ")])}),n("k-dropdown-content",{ref:"dropdown",attrs:{slot:"footer"},on:{open:function(e){t.$nextTick(function(){t.$refs.search.focus()})},close:function(e){t.q=null}},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:{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){t.select(e)}},nativeOn:{keydown:[function(n){if(!("button"in n)&&t._k(n.keyCode,"enter",13,n.key,"Enter"))return null;n.preventDefault(),t.select(e)},function(n){if(!("button"in n)&&t._k(n.keyCode,"space",32,n.key,[" ","Spacebar"]))return null;n.preventDefault(),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("a481"),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}},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(u["a"])({},t,{display:t.text,info:t.value})});var t=new RegExp("(".concat(this.q,")"),"ig");return this.options.filter(function(e){return e.text.match(t)||e.value.match(t)}).map(function(e){return Object(u["a"])({},e,{display:e.text.replace(t,"$1"),info: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),this.$events.$on("keydown.esc",this.escape)},destroyed:function(){this.$events.$off("click",this.close),this.$events.$off("keydown.cmd.s",this.close),this.$events.$off("keydown.esc",this.escape)},methods:{add:function(t){this.addable&&(this.state.push(t),this.onInput())},blur:function(){this.close()},close:function(){this.$refs.dropdown.close(),this.q=null,this.$el.focus()},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();break;case"next":e&&e.nextSibling&&e.nextSibling.focus();break}},onInput:function(){this.$emit("input",this.sorted)},onInvalid:function(){this.$emit("invalid",this.$v.$invalid,this.$v)},remove:function(t){this.state.splice(this.index(t),1),this.onInput()},select:function(t){t={text:t.text,value:t.value},this.isSelected(t)?this.remove(t):this.add(t)}},validations:function(){return{state:{required:!this.required||ts["required"],minLength:!this.min||Object(ts["minLength"])(this.min),maxLength:!this.max||Object(ts["maxLength"])(this.max)}}}}),Bs=Ds,Fs=(n("6a0a"),Object(m["a"])(Bs,Ns,Ps,!1,null,null,null));Fs.options.__file="MultiselectInput.vue";var Rs=Fs.exports,Ms=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"}},"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,value:t.value},!1),t.listeners))},zs=[],Us={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{listeners:Object(u["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()},onInvalid:function(){this.$emit("invalid",this.$v.$invalid,this.$v)},onInput:function(t){this.$emit("input",Number(t))},select:function(){this.$refs.input.select()}},validations:function(){return{value:{required:!this.required||ts["required"],min:!this.min||Object(ts["minValue"])(this.min),max:!this.max||Object(ts["maxValue"])(this.max)}}}},Hs=Us,Vs=(n("4b75"),Object(m["a"])(Hs,Ms,zs,!1,null,null,null));Vs.options.__file="NumberInput.vue";var Ks,Gs,Ys=Vs.exports,Ws={extends:Ts,props:Object(u["a"])({},Ts.props,{autocomplete:{type:String,default:"new-password"},type:{type:String,default:"password"}})},Js=Ws,Xs=Object(m["a"])(Js,Ks,Gs,!1,null,null,null);Xs.options.__file="PasswordInput.vue";var Qs=Xs.exports,Zs=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){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)},to=[],eo={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||ts["required"]}}}},no=eo,io=(n("d11d"),Object(m["a"])(no,Zs,to,!1,null,null,null));io.options.__file="RadioInput.vue";var so=io.exports,oo=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"}},"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,value:t.value},!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()])},ao=[],ro=(n("6b54"),{inheritAttrs:!1,props:{autofocus:Boolean,disabled:Boolean,id:[String,Number],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(u["a"])({},this.$listeners,{input:function(e){return t.onInput(e.target.value)}})}},computed:{label:function(){return null!==this.value?this.format(this.value):"–"},center:function(){var t=(this.max-this.min)/2+this.min;return Math.ceil(t/this.step)*this.step},position:function(){return null!==this.value?this.value:this.center}},watch:{value: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{value:{required:!this.required||ts["required"],min:!this.min||Object(ts["minValue"])(this.min),max:!this.max||Object(ts["maxValue"])(this.max)}}}}),lo=ro,uo=(n("3c0c"),Object(m["a"])(lo,oo,ao,!1,null,null,null));uo.options.__file="RangeInput.vue";var co=uo.exports,po=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({directives:[{name:"model",rawName:"v-model",value:t.selected,expression:"selected"}],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},on:{change:function(e){var n=Array.prototype.filter.call(e.target.options,function(t){return t.selected}).map(function(t){var e="_value"in t?t._value:t.value;return e});t.selected=e.target.multiple?n:n[0]}}},t.listeners),[!1!==t.empty?n("option",{attrs:{value:""}},[t._v(t._s(t.empty))]):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")])},fo=[],ho={inheritAttrs:!1,props:{autofocus:Boolean,ariaLabel:String,disabled:Boolean,id:[Number,String],name:[Number,String],placeholder:String,empty:{type:[String,Boolean],default:"—"},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(u["a"])({},this.$listeners,{click:function(e){return t.onClick(e)},input:function(e){return t.onInput(e.target.value)}})}},computed:{label:function(){var t=this.text(this.selected);return""===this.selected||null===this.selected||null===t?this.placeholder||"—":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||ts["required"]}}}},mo=ho,go=(n("bd46"),Object(m["a"])(mo,po,fo,!1,null,null,null));go.options.__file="SelectInput.vue";var vo=go.exports,bo=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:!0,name:"tag"},on:{remove:function(n){t.remove(e)}},nativeOn:{click:function(t){t.stopPropagation()},blur:function(e){t.selectTag(null)},focus:function(n){t.selectTag(e)},keydown:[function(e){return"button"in e||!t._k(e.keyCode,"left",37,e.key,["Left","ArrowLeft"])?"button"in e&&0!==e.button?null:void t.navigate("prev"):null},function(e){return"button"in e||!t._k(e.keyCode,"right",39,e.key,["Right","ArrowRight"])?"button"in e&&2!==e.button?null:void t.navigate("next"):null}],dblclick:function(n){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){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){t.type(e.target.value)}],blur:[t.blurInput,function(e){t.$forceUpdate()}],keydown:[function(e){return("button"in e||!t._k(e.keyCode,"s",void 0,e.key,void 0))&&e.metaKey?t.blurInput(e):null},function(e){return"button"in e||!t._k(e.keyCode,"left",37,e.key,["Left","ArrowLeft"])?"button"in e&&0!==e.button?null:t.leaveInput(e):null},function(e){return"button"in e||!t._k(e.keyCode,"enter",13,e.key,"Enter")?t.enter(e):null},function(e){return"button"in e||!t._k(e.keyCode,"tab",9,e.key,"Tab")?t.tab(e):null},function(e){return"button"in e||!t._k(e.keyCode,"backspace",void 0,e.key,void 0)?t.leaveInput(e):null}]}})])],1)],2)},ko=[],_o={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.text})}},watch:{value:function(t){this.tags=this.prepareTags(t),this.onInvalid()}},mounted:function(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{addString:function(t){t&&(t=t.trim(),0!==t.length&&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||ts["required"],minLength:!this.min||Object(ts["minLength"])(this.min),maxLength:!this.max||Object(ts["maxLength"])(this.max)}}}},$o=_o,yo=(n("eabd"),Object(m["a"])($o,bo,ko,!1,null,null,null));yo.options.__file="TagsInput.vue";var xo,wo,So=yo.exports,Oo={extends:Ts,props:Object(u["a"])({},Ts.props,{autocomplete:{type:String,default:"tel"},type:{type:String,default:"tel"}})},Co=Oo,Eo=Object(m["a"])(Co,xo,wo,!1,null,null,null);Eo.options.__file="TelInput.vue";var jo=Eo.exports,To=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?n("k-toolbar",{ref:"toolbar",attrs:{buttons:t.buttons},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-size":t.size},on:{focus:t.onFocus,input:t.onInput,keydown:[function(e){return("button"in e||!t._k(e.keyCode,"enter",13,e.key,"Enter"))&&e.metaKey?t.onSubmit(e):null},function(e){return e.metaKey?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-email-dialog",{ref:"emailDialog",on:{cancel:t.cancel,submit:function(e){t.insert(e)}}}),n("k-link-dialog",{ref:"linkDialog",on:{cancel:t.cancel,submit:function(e){t.insert(e)}}})],1)},Io=[],Lo=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("nav",{staticClass:"k-toolbar"},[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){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){t.command(e.command,e.args)}}})]]})],2)])},Ao=[],qo=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")})},No={layout:["headlines","bold","italic","|","link","email","code","|","ul","ol"],props:{buttons:{type:[Boolean,Array],default:!0}},data:function(){var t={},e={},n=[],i=this.commands();return!1===this.buttons?t:(Array.isArray(this.buttons)&&(n=this.buttons),!0!==Array.isArray(this.buttons)&&(n=this.$options.layout),n.forEach(function(n,s){if("|"===n)t["divider-"+s]={divider:!0};else if(i[n]){var o=i[n];t[n]=o,o.shortcut&&(e[o.shortcut]=n)}}),{layout:t,shortcuts:e})},methods:{command:function(t,e){"function"===typeof t?t.apply(this):this.$emit("command",t,e)},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"},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 qo.apply(this,["ul"])}},ol:{label:this.$t("toolbar.button.ol"),icon:"list-numbers",command:function(){return qo.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)}}}},Po=No,Do=(n("813c"),Object(m["a"])(Po,Lo,Ao,!1,null,null,null));Do.options.__file="Toolbar.vue";var Bo=Do.exports,Fo=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){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)},Ro=[],Mo={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(){return this.value.text.length>0?"(email: ".concat(this.value.email," text: ").concat(this.value.text,")"):"(email: ".concat(this.value.email,")")},createMarkdown:function(){return this.value.text.length>0?"[".concat(this.value.text,"](mailto:").concat(this.value.email,")"):"<".concat(this.value.email,">")},submit:function(){this.$emit("submit",this.kirbytext?this.createKirbytext():this.createMarkdown()),this.value={email:null,text:null},this.$refs.dialog.close()}}},zo=Mo,Uo=Object(m["a"])(zo,Fo,Ro,!1,null,null,null);Uo.options.__file="EmailDialog.vue";var Ho=Uo.exports,Vo=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){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)},Ko=[],Go={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()}}},Yo=Go,Wo=Object(m["a"])(Yo,Vo,Ko,!1,null,null,null);Wo.options.__file="LinkDialog.vue";var Jo=Wo.exports,Xo=n("19e9"),Qo=n.n(Xo),Zo={components:{"k-toolbar":Bo,"k-email-dialog":Ho,"k-link-dialog":Jo},inheritAttrs:!1,props:{autofocus:Boolean,buttons:{type:[Boolean,Array],default:!0},disabled:Boolean,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,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(){Qo()(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()},prepend:function(t){this.insert(t+" "+this.selection())},resize:function(){Qo.a.update(this.$refs.input)},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(){var t=this.$store.state.drag;t&&"text"===t.type&&(this.focus(),this.insert(t.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){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&&this.$refs.toolbar&&this.$refs.toolbar.shortcut(t.key,t)},onSubmit:function(t){return this.$emit("submit",t)},select:function(){this.$refs.select()},selection:function(){var t=this.$refs.input,e=t.selectionStart,n=t.selectionEnd;return t.value.substring(e,n)},wrap:function(t){this.insert(t+this.selection()+t)}},validations:function(){return{value:{required:!this.required||ts["required"],minLength:!this.minlength||Object(ts["minLength"])(this.minlength),maxLength:!this.maxlength||Object(ts["maxLength"])(this.maxlength)}}}},ta=Zo,ea=(n("f093"),Object(m["a"])(ta,To,Io,!1,null,null,null));ea.options.__file="TextareaInput.vue";var na=ea.exports,ia=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:"––",empty:"––"},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:"––",empty:"––"},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)},sa=[],oa={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=at(this.hour||0),e=at(this.minute||0),n=this.meridiem||"AM",i=24===this.notation?"".concat(t,":").concat(e,":00"):"".concat(t,":").concat(e,":00 ").concat(n),s=ot()("2000-01-01 "+i);this.$emit("input",s.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:at(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=ot()("2001-01-01 "+t+":00");return!1===e.isValid()?{hour:null,minute:null,meridiem:null}:{hour:e.format(24===this.notation?"H":"h"),minute:this.round(e.format("m")),meridiem:e.format("A")}}}},aa=oa,ra=(n("35ad"),Object(m["a"])(aa,ia,sa,!1,null,null,null));ra.options.__file="TimeInput.vue";var la=ra.exports,ua=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("label",{staticClass:"k-toggle-input"},[n("input",t._g({ref:"input",staticClass:"k-toggle-input-native",attrs:{disabled:t.disabled,id:t.id,type:"checkbox"},domProps:{checked:t.value}},t.listeners)),n("span",{staticClass:"k-toggle-input-label",domProps:{innerHTML:t._s(t.label)}})])},ca=[],pa={inheritAttrs:!1,props:{autofocus:Boolean,disabled:Boolean,id:[Number,String],text:{type:[Array,String],default:function(){return["off","on"]}},required:Boolean,value:Boolean},data:function(){var t=this;return{listeners:Object(u["a"])({},this.$listeners,{change:function(e){return t.onInput(e.target.checked)},keydown:this.onEnter})}},computed:{label:function(){return Array.isArray(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||ts["required"]}}}},da=pa,fa=(n("3a66"),Object(m["a"])(da,ua,ca,!1,null,null,null));fa.options.__file="ToggleInput.vue";var ha,ma,ga=fa.exports,va={extends:Ts,props:Object(u["a"])({},Ts.props,{autocomplete:{type:String,default:"url"},type:{type:String,default:"url"}})},ba=va,ka=Object(m["a"])(ba,ha,ma,!1,null,null,null);ka.options.__file="UrlInput.vue";var _a=ka.exports,$a=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)},ya=[],xa={inheritAttrs:!1,props:Object(u["a"])({},Ii.props,Ui.props,cs.props,{counter:{type:Boolean,default:!0}}),computed:{counterOptions:function(){return null!==this.value&&!this.disabled&&!1!==this.counter&&{count:this.value&&Array.isArray(this.value)?this.value.length:0,min:this.min,max:this.max}}},methods:{focus:function(){this.$refs.input.focus()}}},wa=xa,Sa=Object(m["a"])(wa,$a,ya,!1,null,null,null);Sa.options.__file="CheckboxesField.vue";var Oa=Sa.exports,Ca=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){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)},Ea=[],ja={inheritAttrs:!1,props:Object(u["a"])({},Ii.props,Ui.props,ys.props,{icon:{type:String,default:"calendar"}}),data:function(){return{date:this.value,listeners:Object(u["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)}}},Ta=ja,Ia=Object(m["a"])(Ta,Ca,Ea,!1,null,null,null);Ia.options.__file="DateField.vue";var La=Ia.exports,Aa=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:"mailto:"+t.value,tooltip:t.$t("open"),tabindex:"-1",target:"_blank"},slot:"icon"}):t._e()],1)],1)},qa=[],Na={inheritAttrs:!1,props:Object(u["a"])({},Ii.props,Ui.props,qs.props,{link:{type:Boolean,default:!0},icon:{type:String,default:"email"}}),methods:{focus:function(){this.$refs.input.focus()}}},Pa=Na,Da=Object(m["a"])(Pa,Aa,qa,!1,null,null,null);Da.options.__file="EmailField.vue";var Ba=Da.exports,Fa=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?n("k-button",{attrs:{slot:"options",icon:"add"},on:{click:t.open},slot:"options"},[t._v("\n "+t._s(t.$t("select"))+"\n ")]):t._e(),t.selected.length?[n("k-draggable",{attrs:{element:t.elements.list,list:t.selected,"data-size":t.size,handle:!0},on:{end:t.onInput}},t._l(t.selected,function(e,i){return n(t.elements.item,{key:e.filename,tag:"component",attrs:{sortable:t.selected.length>1,text:e.text,link:e.link,info:e.info,image:e.image,icon:e.icon}},[n("k-button",{attrs:{slot:"options",tooltip:t.$t("remove"),icon:"remove"},on:{click:function(e){t.remove(i)}},slot:"options"})],1)}),1)]:n("k-empty",{attrs:{layout:t.layout,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}})],2)},Ra=[],Ma={inheritAttrs:!1,props:Object(u["a"])({},Ii.props,{empty:String,layout:String,max:Number,multiple:Boolean,parent:String,size: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"]},more:function(){return!this.max||this.max>this.selected.length}},watch:{value:function(t){this.selected=t}},methods:{open:function(){var t=this;return this.$api.get(this.endpoints.field).then(function(e){var n=t.selected.map(function(t){return t.id});e=e.map(function(e){return e.selected=-1!==n.indexOf(e.id),e.thumb=t.image||{},e.thumb.url=!1,e.thumbs&&e.thumbs.tiny&&(e.thumb.url=e.thumbs.medium),e}),t.$refs.selector.open(e,{max:t.max,multiple:t.multiple})}).catch(function(){t.$store.dispatch("notification/error","The files query does not seem to be correct")})},remove:function(t){this.selected.splice(t,1),this.onInput()},focus:function(){},onInput:function(){this.$emit("input",this.selected)},select:function(t){this.selected=t,this.onInput()}}},za=Ma,Ua=Object(m["a"])(za,Fa,Ra,!1,null,null,null);Ua.options.__file="FilesField.vue";var Ha=Ua.exports,Va=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")])},Ka=[],Ga={props:{label:String,numbered:Boolean}},Ya=Ga,Wa=(n("7027"),Object(m["a"])(Ya,Va,Ka,!1,null,null,null));Wa.options.__file="HeadlineField.vue";var Ja=Wa.exports,Xa=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)},Qa=[],Za={props:{label:String,text:String,theme:{type:String,default:"info"}}},tr=Za,er=(n("e104"),Object(m["a"])(tr,Xa,Qa,!1,null,null,null));er.options.__file="InfoField.vue";var nr=er.exports,ir=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("hr",{staticClass:"k-line-field"})},sr=[],or=(n("4e2b"),{}),ar=Object(m["a"])(or,ir,sr,!1,null,null,null);ar.options.__file="LineField.vue";var rr=ar.exports,lr=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}},"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)},ur=[],cr={inheritAttrs:!1,props:Object(u["a"])({},Ii.props,Ui.props,Rs.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&&Array.isArray(this.value)?this.value.length:0,min:this.min,max:this.max}}},methods:{blur:function(t){this.$refs.input.blur(t)},focus:function(){this.$refs.input.focus()}}},pr=cr,dr=Object(m["a"])(pr,lr,ur,!1,null,null,null);dr.options.__file="MultiselectField.vue";var fr=dr.exports,hr=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)},mr=[],gr={inheritAttrs:!1,props:Object(u["a"])({},Ii.props,Ui.props,Ys.props),methods:{focus:function(){this.$refs.input.focus()}}},vr=gr,br=Object(m["a"])(vr,hr,mr,!1,null,null,null);br.options.__file="NumberField.vue";var kr=br.exports,_r=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),[t.more?n("k-button",{attrs:{slot:"options",icon:"add"},on:{click:t.open},slot:"options"},[t._v("\n "+t._s(t.$t("select"))+"\n ")]):t._e(),t.selected.length?[n("k-draggable",{attrs:{element:t.elements.list,handle:!0,list:t.selected,"data-size":t.size},on:{end:t.onInput}},t._l(t.selected,function(e,i){return n(t.elements.item,{key:e.id,tag:"component",attrs:{sortable:t.selected.length>1,text:e.text,info:e.info,link:e.link,icon:e.icon,image:e.image}},[n("k-button",{attrs:{slot:"options",icon:"remove"},on:{click:function(e){t.remove(i)}},slot:"options"})],1)}),1)]:n("k-empty",{attrs:{layout:t.layout,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)},$r=[],yr=function(t){if(void 0!==t)return JSON.parse(JSON.stringify(t))},xr={inheritAttrs:!1,props:Object(u["a"])({},Ii.props,{empty:String,layout:String,max:Number,multiple:Boolean,size: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"]},more:function(){return!this.max||this.max>this.selected.length}},watch:{value:function(t){this.selected=t}},methods:{open:function(){this.$refs.selector.open({endpoint:this.endpoints.field,max:this.max,multiple:this.multiple,selected:yr(this.selected)})},remove:function(t){this.selected.splice(t,1),this.onInput()},focus:function(){},onInput:function(){this.$emit("input",this.selected)},select:function(t){this.selected=t,this.onInput()}}},wr=xr,Sr=Object(m["a"])(wr,_r,$r,!1,null,null,null);Sr.options.__file="PagesField.vue";var Or=Sr.exports,Cr=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)},Er=[],jr={inheritAttrs:!1,props:Object(u["a"])({},Ii.props,Ui.props,Qs.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()}}},Tr=jr,Ir=Object(m["a"])(Tr,Cr,Er,!1,null,null,null);Ir.options.__file="PasswordField.vue";var Lr=Ir.exports,Ar=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)},qr=[],Nr={inheritAttrs:!1,props:Object(u["a"])({},Ii.props,Ui.props,so.props),methods:{focus:function(){this.$refs.input.focus()}}},Pr=Nr,Dr=Object(m["a"])(Pr,Ar,qr,!1,null,null,null);Dr.options.__file="RadioField.vue";var Br=Dr.exports,Fr=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)},Rr=[],Mr={inheritAttrs:!1,props:Object(u["a"])({},Ii.props,Ui.props,co.props),methods:{focus:function(){this.$refs.input.focus()}}},zr=Mr,Ur=Object(m["a"])(zr,Fr,Rr,!1,null,null,null);Ur.options.__file="RangeField.vue";var Hr=Ur.exports,Vr=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)},Kr=[],Gr={inheritAttrs:!1,props:Object(u["a"])({},Ii.props,Ui.props,vo.props,{icon:{type:String,default:"angle-down"}}),methods:{focus:function(){this.$refs.input.focus()}}},Yr=Gr,Wr=Object(m["a"])(Yr,Vr,Kr,!1,null,null,null);Wr.options.__file="SelectField.vue";var Jr=Wr.exports,Xr=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:{icon:"list-bullet"},on:{click:t.add}},[t._v("\n "+t._s(t.$t("field.structure.empty"))+"\n ")]):[n("table",{staticClass:"k-structure-table",attrs:{"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",attrs:{"data-width":e.width,"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,o){return n("td",{key:o,staticClass:"k-structure-table-column",attrs:{title:s.label,"data-width":s.width,"data-align":s.align},on:{click:function(e){t.jump(i,o)}}},[!1===t.columnIsEmpty(e[o])?[t.previewExists(s.type)?n("k-"+s.type+"-field-preview",{tag:"component",attrs:{value:e[o],column:s,field:t.fields[o]}}):[n("p",{staticClass:"k-structure-table-text"},[t._v("\n "+t._s(s.before)+" "+t._s(t.displayText(t.fields[o],e[o])||"–")+" "+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){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)},Qr=[],Zr=(n("8615"),function(t){t=t||{};var e=t.desc?-1:1,n=-e,i=/^0/,s=/\s+/g,o=/^\s+|\s+$/g,a=/[^\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 p(""+t).replace(o,"")}:function(t){return(""+t).replace(o,"")};function p(t){return t.toLocaleLowerCase?t.toLocaleLowerCase():t.toLowerCase()}function d(t){return t.replace(l,"\0$1\0").replace(/\0$/,"").replace(/^\0/,"").split("\0")}function f(t,e){return(!t.match(i)||1===e)&&parseFloat(t)||t.replace(s," ").replace(o,"")||0}return function(t,i){var s=c(t),o=c(i);if(!s&&!o)return 0;if(!s&&o)return n;if(s&&!o)return e;var l=d(s),p=d(o),h=parseInt(s.match(r),16)||1!==l.length&&Date.parse(s),m=parseInt(o.match(r),16)||h&&o.match(u)&&Date.parse(o)||null;if(m){if(hm)return e}for(var g=l.length,v=p.length,b=0,k=Math.max(g,v);b0)return e;if(y<0)return n;if(b===k-1)return 0}else{if(_<$)return n;if(_>$)return e}}return 0}});Array.prototype.sortBy=function(t){var e=Zr(),n=t.split(" "),i=n[0],s=n[1]||"asc";return this.sort(function(t,n){var o=String(t[i]).toLowerCase(),a=String(n[i]).toLowerCase();return"desc"===s?e(a,o):e(o,a)})};var tl={inheritAttrs:!1,props:Object(u["a"])({},Ii.props,{columns:Object,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 Object.keys(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)},isSortable:function(){return!this.sortBy&&(!this.limit&&(!0!==this.disabled&&(!(this.items.length<=1)&&!1!==this.sortable)))},pagination:function(){return{page:this.page,limit:this.limit,total:this.items.length,align:"center",details:!0}},paginatedItems:function(){if(!this.limit)return this.items;var t=this.page-1,e=t*this.limit;return this.items.slice(e,e+this.limit)}},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={};Object.keys(this.fields).forEach(function(n){var i=t.fields[n];i.default&&(e[n]=i.default)}),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("form/unlock")},columnIsEmpty:function(t){return void 0===t||null===t||""===t||("object"===Object(K["a"])(t)&&0===Object.keys(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("form/lock"),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=ot()(e);return n.isValid()?n.format("YYYY-MM-DD"):"";case"tags":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"select":var i=t.options.filter(function(t){return t.value===e})[0];return i?i.text:null}return"object"===Object(K["a"])(e)&&null!==e?"…":e},escape:function(){var t=this;if("new"===this.currentIndex){var e=Object.values(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.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,e)},makeItems:function(t){return!1===Array.isArray(t)?[]:this.sort(t)},onInput:function(){this.$emit("input",this.items)},open:function(t,e){this.currentIndex=t,this.currentModel=yr(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!==i["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}):Promise.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})}}},el=tl,nl=(n("68b5"),Object(m["a"])(el,Xr,Qr,!1,null,null,null));nl.options.__file="StructureField.vue";var il=nl.exports,sl=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)},ol=[],al={inheritAttrs:!1,props:Object(u["a"])({},Ii.props,Ui.props,So.props,{counter:{type:Boolean,default:!0}}),computed:{counterOptions:function(){return null!==this.value&&!this.disabled&&!1!==this.counter&&{count:this.value&&Array.isArray(this.value)?this.value.length:0,min:this.min,max:this.max}}},methods:{focus:function(){this.$refs.input.focus()}}},rl=al,ll=Object(m["a"])(rl,sl,ol,!1,null,null,null);ll.options.__file="TagsField.vue";var ul=ll.exports,cl=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)},pl=[],dl={inheritAttrs:!1,props:Object(u["a"])({},Ii.props,Ui.props,jo.props,{icon:{type:String,default:"phone"}}),methods:{focus:function(){this.$refs.input.focus()}}},fl=dl,hl=Object(m["a"])(fl,cl,pl,!1,null,null,null);hl.options.__file="TelField.vue";var ml=hl.exports,gl=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),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field"}},"k-input",t.$props,!1),t.$listeners))],1)},vl=[],bl={inheritAttrs:!1,props:Object(u["a"])({},Ii.props,Ui.props,Ts.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()}}},kl=bl,_l=(n("a89c"),Object(m["a"])(kl,gl,vl,!1,null,null,null));_l.options.__file="TextField.vue";var $l=_l.exports,yl=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)},xl=[],wl={inheritAttrs:!1,props:Object(u["a"])({},Ii.props,Ui.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()}}},Sl=wl,Ol=Object(m["a"])(Sl,yl,xl,!1,null,null,null);Ol.options.__file="TextareaField.vue";var Cl=Ol.exports,El=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)},jl=[],Tl={inheritAttrs:!1,props:Object(u["a"])({},Ii.props,Ui.props,la.props,{icon:{type:String,default:"clock"}}),methods:{focus:function(){this.$refs.input.focus()}}},Il=Tl,Ll=Object(m["a"])(Il,El,jl,!1,null,null,null);Ll.options.__file="TimeField.vue";var Al=Ll.exports,ql=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)},Nl=[],Pl={inheritAttrs:!1,props:Object(u["a"])({},Ii.props,Ui.props,ga.props),methods:{focus:function(){this.$refs.input.focus()}}},Dl=Pl,Bl=Object(m["a"])(Dl,ql,Nl,!1,null,null,null);Bl.options.__file="ToggleField.vue";var Fl=Bl.exports,Rl=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)},Ml=[],zl={inheritAttrs:!1,props:Object(u["a"])({},Ii.props,Ui.props,_a.props,{link:{type:Boolean,default:!0},icon:{type:String,default:"url"}}),methods:{focus:function(){this.$refs.input.focus()}}},Ul=zl,Hl=Object(m["a"])(Ul,Rl,Ml,!1,null,null,null);Hl.options.__file="UrlField.vue";var Vl=Hl.exports,Kl=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),[t.more?n("k-button",{attrs:{slot:"options",icon:"add"},on:{click:t.open},slot:"options"},[t._v("\n "+t._s(t.$t("select"))+"\n ")]):t._e(),t.selected.length?[n("k-draggable",{attrs:{element:t.elements.list,list:t.selected,handle:!0},on:{end:t.onInput}},t._l(t.selected,function(e,i){return n(t.elements.item,{key:e.email,tag:"component",attrs:{sortable:!0,text:e.username,link:t.$api.users.link(e.id),image:e.avatar?{url:e.avatar.url,back:"pattern",cover:!0}:null,icon:{type:"user",back:"black"}}},[n("k-button",{attrs:{slot:"options",icon:"remove"},on:{click:function(e){t.remove(i)}},slot:"options"})],1)}),1)]:n("k-empty",{attrs:{icon:"users"},on:{click:t.open}},[t._v("\n "+t._s(t.$t("field.users.empty"))+"\n ")]),n("k-users-dialog",{ref:"selector",on:{submit:t.select}})],2)},Gl=[],Yl={inheritAttrs:!1,props:Object(u["a"])({},Ii.props,{max:Number,multiple:Boolean,value:{type:Array,default:function(){return[]}}}),data:function(){return{layout:"list",selected:this.value}},computed:{elements:function(){return{list:"k-list",item:"k-list-item"}},more:function(){return!this.max||this.max>this.selected.length}},watch:{value:function(t){this.selected=t}},methods:{open:function(){this.$refs.selector.open({max:this.max,multiple:this.multiple,selected:this.selected.map(function(t){return t.email})})},remove:function(t){this.selected.splice(t,1),this.onInput()},focus:function(){},onInput:function(){this.$emit("input",this.selected)},select:function(t){this.selected=t,this.onInput()}}},Wl=Yl,Jl=Object(m["a"])(Wl,Kl,Gl,!1,null,null,null);Jl.options.__file="UsersField.vue";var Xl=Jl.exports,Ql=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.value?n("ul",{staticClass:"k-files-field-preview"},t._l(t.value,function(t){return n("li",{key:t.url},[n("k-link",{attrs:{title:t.filename,to:t.link},nativeOn:{click:function(t){t.stopPropagation()}}},[n("k-image",{attrs:{src:t.url,back:"pattern"}})],1)],1)}),0):t._e()},Zl=[],tu={props:{value:Array}},eu=tu,nu=(n("3e93"),Object(m["a"])(eu,Ql,Zl,!1,null,null,null));nu.options.__file="FilesFieldPreview.vue";var iu=nu.exports,su=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("p",{staticClass:"k-url-field-preview"},[n("k-link",{attrs:{to:t.link,target:"_blank"},nativeOn:{click:function(t){t.stopPropagation()}}},[t._v(t._s(t.value))])],1)},ou=[],au={props:{column:Object,value:String},computed:{link:function(){return this.value}}},ru=au,lu=(n("b61e"),Object(m["a"])(ru,su,ou,!1,null,null,null));lu.options.__file="UrlFieldPreview.vue";var uu,cu,pu=lu.exports,du={extends:pu,computed:{link:function(){return"mailto:"+this.value}}},fu=du,hu=Object(m["a"])(fu,uu,cu,!1,null,null,null);hu.options.__file="EmailFieldPreview.vue";var mu=hu.exports,gu=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()},vu=[],bu={props:{value:Array}},ku=bu,_u=(n("0eae"),Object(m["a"])(ku,gu,vu,!1,null,null,null));_u.options.__file="PagesFieldPreview.vue";var $u=_u.exports,yu=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()},xu=[],wu={props:{value:Array}},Su=wu,Ou=(n("77f7"),Object(m["a"])(Su,yu,xu,!1,null,null,null));Ou.options.__file="UsersFieldPreview.vue";var Cu=Ou.exports;i["a"].use(T.a),i["a"].use(L);var Eu={install:function(t){t.filter("t",function(t){return t}),t.directive("tab",{inserted:function(t){t.addEventListener("keyup",function(e){9===e.keyCode&&(t.dataset.tabbed=!0)}),t.addEventListener("blur",function(){delete t.dataset.tabbed})}}),t.component("k-bar",D),t.component("k-box",U),t.component("k-button",J),t.component("k-button-group",et),t.component("k-calendar",ct),t.component("k-card",vt),t.component("k-cards",xt),t.component("k-collection",jt),t.component("k-column",Nt),t.component("k-counter",Mt),t.component("k-dialog",Gt),t.component("k-draggable",ee),t.component("k-dropdown",ae),t.component("k-dropdown-content",fe),t.component("k-dropdown-item",ke),t.component("k-empty",Ce),t.component("k-error-boundary",Ie),t.component("k-grid",De),t.component("k-header",Ue),t.component("k-headline",We),t.component("k-icon",en),t.component("k-image",ln),t.component("k-link",hn),t.component("k-list",_n),t.component("k-list-item",On),t.component("k-pagination",Ln),t.component("k-prev-next",Bn),t.component("k-progress",Hn),t.component("k-sort-handle",Wn),t.component("k-tag",ei),t.component("k-text",ri),t.component("k-view",fi),t.component("k-autocomplete",ki),t.component("k-form",Si),t.component("k-field",Ii),t.component("k-fieldset",Di),t.component("k-input",Ui),t.component("k-upload",Xi),t.component("k-checkbox-input",ss),t.component("k-checkboxes-input",cs),t.component("k-date-input",gs),t.component("k-datetime-input",ys),t.component("k-email-input",qs),t.component("k-multiselect-input",Rs),t.component("k-number-input",Ys),t.component("k-password-input",Qs),t.component("k-radio-input",so),t.component("k-range-input",co),t.component("k-select-input",vo),t.component("k-tags-input",So),t.component("k-tel-input",jo),t.component("k-text-input",Ts),t.component("k-textarea-input",na),t.component("k-time-input",la),t.component("k-toggle-input",ga),t.component("k-url-input",_a),t.component("k-checkboxes-field",Oa),t.component("k-date-field",La),t.component("k-email-field",Ba),t.component("k-files-field",Ha),t.component("k-headline-field",Ja),t.component("k-info-field",nr),t.component("k-line-field",rr),t.component("k-multiselect-field",fr),t.component("k-number-field",kr),t.component("k-pages-field",Or),t.component("k-password-field",Lr),t.component("k-radio-field",Br),t.component("k-range-field",Hr),t.component("k-select-field",Jr),t.component("k-structure-field",il),t.component("k-tags-field",ul),t.component("k-text-field",$l),t.component("k-textarea-field",Cl),t.component("k-tel-field",ml),t.component("k-time-field",Al),t.component("k-toggle-field",Fl),t.component("k-url-field",Vl),t.component("k-users-field",Xl),t.component("k-email-field-preview",mu),t.component("k-files-field-preview",iu),t.component("k-pages-field-preview",$u),t.component("k-url-field-preview",pu),t.component("k-users-field-preview",Cu)}};i["a"].use(Eu);var ju,Tu,Iu={extends:Gt,created:function(){this.$events.$on("keydown.esc",this.close,!1)},destroyed:function(){this.$events.$off("keydown.esc",this.close,!1)}},Lu=Iu,Au=Object(m["a"])(Lu,ju,Tu,!1,null,null,null);Au.options.__file="Dialog.vue";var qu=Au.exports,Nu=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.error?n("k-dialog",{ref:"dialog",staticClass:"k-error-dialog",attrs:{visible:!0},on:{close:t.exit,open:t.enter}},[n("k-text",[t._v(t._s(t.error.message))]),t.error.details&&Object.keys(t.error.details).length?n("dl",{staticClass:"k-error-details"},[t._l(t.error.details,function(e,i){return[n("dt",{key:"detail-label-"+i},[t._v(t._s(e.label))]),n("dd",{key:"detail-message-"+i},["object"===typeof e.message?[n("ul",t._l(e.message,function(e,i){return n("li",{key:i},[t._v("\n "+t._s(e)+"\n ")])}),0)]:[t._v("\n "+t._s(e.message)+"\n ")]],2)]})],2):t._e(),n("k-button-group",{attrs:{slot:"footer"},slot:"footer"},[n("k-button",{attrs:{icon:"check"},on:{click:t.close}},[t._v("\n "+t._s(t.$t("confirm"))+"\n ")])],1)],1):t._e()},Pu=[],Du={mixins:[_],computed:{error:function(){var t=this.$store.state.notification;return"error"===t.type?t:null}},methods:{enter:function(){var t=this;this.$nextTick(function(){t.$el.querySelector(".k-dialog-footer .k-button").focus()})},exit:function(){this.$store.dispatch("notification/close")}}},Bu=Du,Fu=(n("7737"),Object(m["a"])(Bu,Nu,Pu,!1,null,null,null));Fu.options.__file="ErrorDialog.vue";var Ru=Fu.exports,Mu=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("file.delete.confirm",{filename:t.filename}))}})],1)},zu=[],Uu={mixins:[_],data:function(){return{id:null,parent:null,filename:null}},methods:{open:function(t,e){var n=this;this.$api.files.get(t,e).then(function(e){n.id=e.id,n.filename=e.filename,n.parent=t,n.$refs.dialog.open()}).catch(function(t){n.$store.dispatch("notification/error",t)})},submit:function(){var t=this;this.$api.files.delete(this.parent,this.filename).then(function(){t.$store.dispatch("form/remove","files/"+t.id),t.$store.dispatch("notification/success",":)"),t.$events.$emit("file.delete"),t.$emit("success"),t.$refs.dialog.close()}).catch(function(e){t.$refs.dialog.error(e.message)})}}},Hu=Uu,Vu=Object(m["a"])(Hu,Mu,zu,!1,null,null,null);Vu.options.__file="FileRemoveDialog.vue";var Ku=Vu.exports,Gu=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){t.$refs.form.submit()}}},[n("k-form",{ref:"form",attrs:{fields:t.fields},on:{submit:t.submit,input:function(e){t.file.name=t.sluggify(t.file.name)}},model:{value:t.file,callback:function(e){t.file=e},expression:"file"}})],1)},Yu=[],Wu=n("b747"),Ju=n.n(Wu),Xu=function(t){return Ju()(t,{remove:/[$*_+~.,;:()'"`!?§$%\/=#@]/g}).toLowerCase()},Qu={mixins:[_],data:function(){return{parent:null,file:{id:null,name:null,filename:null,extension:null}}},computed:{fields:function(){return{name:{label:this.$t("name"),type:"text",required:!0,icon:"title",after:"."+this.file.extension,preselect:!0}}}},methods:{open:function(t,e){var n=this;this.$api.files.get(t,e,{select:["id","filename","name","extension"]}).then(function(e){n.file=e,n.parent=t,n.$refs.dialog.open()}).catch(function(t){n.$store.dispatch("notification/error",t)})},sluggify:function(t){return Xu(t)},submit:function(){var t=this;this.$api.files.rename(this.parent,this.file.filename,this.file.name).then(function(e){t.$store.dispatch("form/revert","files/"+t.file.id),t.$store.dispatch("notification/success",":)"),t.$emit("success",e),t.$events.$emit("file.changeName",e),t.$refs.dialog.close()}).catch(function(e){t.$refs.dialog.error(e.message)})}}},Zu=Qu,tc=Object(m["a"])(Zu,Gu,Yu,!1,null,null,null);tc.options.__file="FileRenameDialog.vue";var ec=tc.exports,nc=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",staticClass:"k-files-dialog",attrs:{size:"medium"},on:{cancel:function(e){t.$emit("cancel")},submit:t.submit}},[t.issue?[n("k-box",{attrs:{text:t.issue,theme:"negative"}})]:[t.files.length?n("k-list",t._l(t.files,function(e,i){return n("k-list-item",{key:e.filename,attrs:{text:e.filename,image:e.image,icon:e.icon},on:{click:function(e){t.toggle(i)}}},[e.selected?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-empty",{attrs:{icon:"image"}},[t._v("\n No files to select\n ")])]],2)},ic=[],sc={data:function(){return{files:[],issue:null,options:{max:null,multiple:!0,api:null,selected:[]}}},computed:{multiple:function(){return!0===this.options.multiple&&1!==this.options.max},checkedIcon:function(){return!0===this.multiple?"check":"circle-filled"}},methods:{selected:function(){return this.files.filter(function(t){return t.selected})},submit:function(){this.$emit("submit",this.selected()),this.$refs.dialog.close()},toggle:function(t){if(!1===this.multiple)this.files=this.files.map(function(t){return t.selected=!1,t}),this.files[t].selected=!0;else if(this.files[t].selected)this.files[t].selected=!1;else{if(this.options.max&&this.options.max<=this.selected().length)return;this.files[t].selected=!0}},open:function(t,e){this.files=t,this.options=e,this.$refs.dialog.open()}}},oc=sc,ac=(n("bf53"),Object(m["a"])(oc,nc,ic,!1,null,null,null));ac.options.__file="FilesDialog.vue";var rc=ac.exports,lc=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",attrs:{button:t.$t("language.create"),notification:t.notification,theme:"positive",size:"medium"},on:{submit:function(e){t.$refs.form.submit()}}},[n("k-form",{ref:"form",attrs:{fields:t.fields,novalidate:!0},on:{submit:t.submit},model:{value:t.language,callback:function(e){t.language=e},expression:"language"}})],1)},uc=[],cc={mixins:[_],data:function(){return{notification:null,language:{name:"",code:"",direction:"ltr"}}},computed:{fields:function(){return{name:{label:this.$t("language.name"),type:"text",required:!0,icon:"title"},code:{label:this.$t("language.code"),type:"text",required:!0,counter:!1,icon:"globe",width:"1/2"},direction:{label:this.$t("language.direction"),type:"select",required:!0,empty:!1,options:[{value:"ltr",text:this.$t("language.direction.ltr")},{value:"rtl",text:this.$t("language.direction.rtl")}],width:"1/2"},locale:{label:this.$t("language.locale"),type:"text",placeholder:"en_US"}}}},watch:{"language.name":function(t){this.language.code=Xu(t).substr(0,2)},"language.code":function(t){this.language.code=Xu(t)}},methods:{open:function(){this.language={name:"",code:"",direction:"ltr"},this.$refs.dialog.open()},submit:function(){var t=this;this.$api.post("languages",this.language).then(function(){t.$store.dispatch("languages/load"),t.success({message:t.$t("language.created"),event:"language.create"})}).catch(function(e){t.$refs.dialog.error(e.message)})}}},pc=cc,dc=Object(m["a"])(pc,lc,uc,!1,null,null,null);dc.options.__file="LanguageCreateDialog.vue";var fc=dc.exports,hc=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("language.delete.confirm",{name:t.language.name}))}})],1)},mc=[],gc={mixins:[_],data:function(){return{language:{name:null}}},methods:{open:function(t){var e=this;this.$api.get("languages/"+t).then(function(t){e.language=t,e.$refs.dialog.open()}).catch(function(t){e.$store.dispatch("notification/error",t)})},submit:function(){var t=this;this.$api.delete("languages/"+this.language.code).then(function(){t.$store.dispatch("languages/load"),t.success({message:t.$t("language.deleted"),event:"language.delete"})}).catch(function(e){t.$refs.dialog.error(e.message)})}}},vc=gc,bc=Object(m["a"])(vc,hc,mc,!1,null,null,null);bc.options.__file="LanguageRemoveDialog.vue";var kc=bc.exports,_c=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",attrs:{button:t.$t("save"),notification:t.notification,size:"medium"},on:{submit:function(e){t.$refs.form.submit()}}},[n("k-form",{ref:"form",attrs:{fields:t.fields},on:{submit:t.submit},model:{value:t.language,callback:function(e){t.language=e},expression:"language"}})],1)},$c=[],yc={mixins:[fc],computed:{fields:function(){var t=fc.computed.fields.apply(this);return t.code.disabled=!0,t}},methods:{open:function(t){var e=this;this.$api.get("languages/"+t).then(function(t){e.language=t,e.$refs.dialog.open()}).catch(function(t){e.$store.dispatch("notification/error",t)})},submit:function(){var t=this;this.$api.patch("languages/"+this.language.code,this.language).then(function(){t.$store.dispatch("languages/load"),t.success({message:t.$t("language.updated"),event:"language.update"})}).catch(function(e){t.$refs.dialog.error(e.message)})}}},xc=yc,wc=Object(m["a"])(xc,_c,$c,!1,null,null,null);wc.options.__file="LanguageUpdateDialog.vue";var Sc=wc.exports,Oc=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",attrs:{button:t.$t("page.draft.create"),notification:t.notification,size:"medium",theme:"positive"},on:{submit:function(e){t.$refs.form.submit()}}},[n("k-form",{ref:"form",attrs:{fields:t.fields,novalidate:!0},on:{submit:t.submit},model:{value:t.page,callback:function(e){t.page=e},expression:"page"}})],1)},Cc=[],Ec={mixins:[_],data:function(){return{notification:null,parent:null,section:null,templates:[],page:{title:"",slug:"",template:null}}},computed:{fields:function(){return{title:{label:this.$t("title"),type:"text",required:!0,icon:"title"},slug:{label:this.$t("slug"),type:"text",required:!0,counter:!1,icon:"url"},template:{name:"template",label:this.$t("template"),type:"select",disabled:1===this.templates.length,required:!0,icon:"code",empty:!1,options:this.templates}}}},watch:{"page.title":function(t){this.page.slug=Xu(t)}},methods:{open:function(t,e,n){var i=this;this.parent=t,this.section=n,this.$api.get(e,{section:n}).then(function(t){i.templates=t.map(function(t){return{value:t.name,text:t.title}}),i.templates[0]&&(i.page.template=i.templates[0].value),i.$refs.dialog.open()}).catch(function(t){i.$store.dispatch("notification/error",t)})},submit:function(){var t=this;if(0===this.page.title.length)return this.$refs.dialog.error("Please enter a title"),!1;var e={template:this.page.template,slug:this.page.slug,content:{title:this.page.title}};this.$api.post(this.parent+"/children",e).then(function(e){t.success({route:t.$api.pages.link(e.id),message:":)",event:"page.create"})}).catch(function(e){t.$refs.dialog.error(e.message)})}}},jc=Ec,Tc=Object(m["a"])(jc,Oc,Cc,!1,null,null,null);Tc.options.__file="PageCreateDialog.vue";var Ic=Tc.exports,Lc=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",attrs:{button:t.$t("delete"),size:t.hasSubpages?"medium":"small",theme:"negative",icon:"trash"},on:{submit:t.submit}},[t.page.hasChildren||t.page.hasDrafts?[n("k-text",{domProps:{innerHTML:t._s(t.$t("page.delete.confirm",{title:t.page.title}))}}),n("div",{staticClass:"k-page-remove-warning"},[n("k-box",{attrs:{theme:"negative"},domProps:{innerHTML:t._s(t.$t("page.delete.confirm.subpages"))}})],1),t.hasSubpages?n("k-form",{attrs:{fields:t.fields},on:{submit:t.submit},model:{value:t.model,callback:function(e){t.model=e},expression:"model"}}):t._e()]:[n("k-text",{domProps:{innerHTML:t._s(t.$t("page.delete.confirm",{title:t.page.title}))},on:{keydown:function(e){return"button"in e||!t._k(e.keyCode,"enter",13,e.key,"Enter")?t.submit(e):null}}})]],2)},Ac=[],qc={mixins:[_],data:function(){return{page:{title:null,hasChildren:!1,hasDrafts:!1},model:{check:null}}},computed:{hasSubpages:function(){return this.page.hasChildren||this.page.hasDrafts},fields:function(){return{check:{label:this.$t("page.delete.confirm.title"),type:"text",counter:!1}}}},methods:{open:function(t){var e=this;this.$api.pages.get(t,{select:"id, title, hasChildren, hasDrafts, parent"}).then(function(t){e.page=t,e.$refs.dialog.open()}).catch(function(t){e.$store.dispatch("notification/error",t)})},submit:function(){var t=this;this.hasSubpages&&this.model.check!==this.page.title?this.$refs.dialog.error(this.$t("error.page.delete.confirm")):this.$api.pages.delete(this.page.id,{force:!0}).then(function(){t.$store.dispatch("form/remove","pages/"+t.page.id);var e={message:":)",event:"page.delete"};t.$route.params.path&&t.page.id===t.$route.params.path.replace(/\+/g,"/")&&(t.page.parent?e.route="/pages/"+t.page.parent.id:e.route="/pages"),t.success(e)}).catch(function(e){t.$refs.dialog.error(e.message)})}}},Nc=qc,Pc=(n("12fb"),Object(m["a"])(Nc,Lc,Ac,!1,null,null,null));Pc.options.__file="PageRemoveDialog.vue";var Dc=Pc.exports,Bc=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){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)},Fc=[],Rc={mixins:[_],data:function(){return{page:{id:null,title:null}}},computed:{fields:function(){return{title:{label:this.$t("title"),type:"text",required:!0,icon:"title",preselect:!0}}}},methods:{open:function(t){var e=this;this.$api.pages.get(t,{select:["id","title"]}).then(function(t){e.page=t,e.$refs.dialog.open()}).catch(function(t){e.$store.dispatch("notification/error",t)})},submit:function(){var t=this;0!==this.page.title.length?this.$api.pages.title(this.page.id,this.page.title).then(function(){t.success({message:":)",event:"page.changeTitle"})}).catch(function(e){t.$refs.dialog.error(e.message)}):this.$refs.dialog.error(this.$t("error.page.changeTitle.empty"))}}},Mc=Rc,zc=Object(m["a"])(Mc,Bc,Fc,!1,null,null,null);zc.options.__file="PageRenameDialog.vue";var Uc=zc.exports,Hc=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:t.submit}},[n("k-form",{ref:"form",attrs:{fields:t.fields},on:{submit:t.changeStatus},model:{value:t.form,callback:function(e){t.form=e},expression:"form"}})],1)},Vc=[],Kc={mixins:[_],data:function(){return{page:{id:null},isBlocked:!1,isIncomplete:!1,form:{status:null,position:null},states:{}}},computed:{fields:function(){var t=this,e={status:{name:"status",label:this.$t("page.changeStatus.select"),type:"radio",required:!0,options:Object.keys(this.states).map(function(e){return{value:e,text:t.states[e].label,info:t.states[e].text}})}};return"listed"===this.form.status&&"default"===this.page.blueprint.num&&(e.position={name:"position",label:this.$t("page.changeStatus.position"),type:"select",empty:!1,options:this.sortingOptions()}),e}},methods:{sortingOptions:function(){var t=this,e=[],n=0;return this.page.siblings.forEach(function(i){if(i.id===t.page.id||i.num<1)return!1;n++,e.push({value:n,text:n}),e.push({value:i.id,text:i.title,disabled:!0})}),e.push({value:n+1,text:n+1}),e},open:function(t){var e=this;this.$api.pages.get(t,{select:["id","status","num","errors","siblings","blueprint"]}).then(function(t){return!1===t.blueprint.options.changeStatus?e.$store.dispatch("notification/error",{message:e.$t("error.page.changeStatus.permission")}):"draft"===t.status&&Object.keys(t.errors).length>0?e.$store.dispatch("notification/error",{message:e.$t("error.page.changeStatus.incomplete"),details:t.errors}):(e.states=t.blueprint.status,e.page=t,e.form.status=t.status,e.form.position=t.num,void e.$refs.dialog.open())}).catch(function(t){e.$store.dispatch("notification/error",t)})},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)})}}},Gc=Kc,Yc=Object(m["a"])(Gc,Hc,Vc,!1,null,null,null);Yc.options.__file="PageStatusDialog.vue";var Wc=Yc.exports,Jc=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){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)},Xc=[],Qc={mixins:[_],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)})}}},Zc=Qc,tp=Object(m["a"])(Zc,Jc,Xc,!1,null,null,null);tp.options.__file="PageTemplateDialog.vue";var ep=tp.exports,np=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){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){t.sluggify(e)}}},"k-text-field",t.field,!1),[n("k-button",{attrs:{slot:"options",icon:"wand","data-options":""},on:{click:function(e){t.sluggify(t.page.title)}},slot:"options"},[t._v("\n "+t._s(t.$t("page.changeSlug.fromTitle"))+"\n ")])],1)],1)],1)},ip=[],sp={mixins:[_],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,preselect:!0}}},methods:{sluggify:function(t){this.slug=Xu(t),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("form/revert","pages/"+t.page.id);var n={message:":)",event:"page.changeSlug"};t.$route.params.path&&t.page.id===t.$route.params.path.replace(/\+/g,"/")&&(n.route=t.$api.pages.link(e.id)),t.success(n)}).catch(function(e){t.$refs.dialog.error(e.message)}):this.$refs.dialog.error(this.$t("error.page.slug.invalid"))}}},op=sp,ap=Object(m["a"])(op,np,ip,!1,null,null,null);ap.options.__file="PageUrlDialog.vue";var rp=ap.exports,lp=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){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.pages.length?n("k-list",t._l(t.pages,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){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){n.stopPropagation(),t.go(e)}}}):t._e()],1)],2)}),1):n("k-empty",{attrs:{icon:"page"}},[t._v("\n No pages to select\n ")])]],2)},up=[],cp={data:function(){return{model:{title:null,parent:null},pages:[],issue:null,options:{endpoint:null,max:null,multiple:!0,parent:null,selected:[]}}},computed:{multiple:function(){return!0===this.options.multiple&&1!==this.options.max},checkedIcon:function(){return!0===this.multiple?"check":"circle-filled"}},methods:{fetch:function(){var t=this;return this.$api.get(this.options.endpoint,{parent:this.options.parent}).then(function(e){t.model=e.model,t.pages=e.pages}).catch(function(e){t.pages=[],t.issue=e.message})},back:function(){this.options.parent=this.model.parent?this.model.parent.id:null,this.fetch()},submit:function(){this.$emit("submit",this.options.selected),this.$refs.dialog.close()},isSelected:function(t){return this.options.selected.map(function(t){return t.id}).includes(t.id)},toggle:function(t){if(!1===this.options.multiple&&(this.options.selected=[]),!1===this.isSelected(t)){if(this.options.max&&this.options.max<=this.options.selected.length)return;this.options.selected.push(t)}else this.options.selected=this.options.selected.filter(function(e){return e.id!==t.id})},open:function(t){var e=this;this.options=t,this.fetch().then(function(){e.$refs.dialog.open()})},go:function(t){this.options.parent=t.id,this.fetch()}}},pp=cp,dp=(n("ac27"),Object(m["a"])(pp,lp,up,!1,null,null,null));dp.options.__file="PagesDialog.vue";var fp,hp,mp=dp.exports,gp={extends:Uc,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.$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)})}}},vp=gp,bp=Object(m["a"])(vp,fp,hp,!1,null,null,null);bp.options.__file="SiteRenameDialog.vue";var kp=bp.exports,_p=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){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)},$p=[],yp={mixins:[_],data:function(){return{user:this.emptyUser(),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)})},emptyUser:function(){return{name:"",email:"",password:"",language:"en",role:"admin"}},open:function(){var t=this;this.$api.roles.options().then(function(e){t.roles=e,t.$api.translations.options().then(function(e){t.languages=e,t.$refs.dialog.open()}).catch(function(e){t.$store.dispatch("notification/error",e)})}).catch(function(e){t.$store.dispatch("notification/error",e)})},reset:function(){this.user=this.emptyUser()}}},xp=yp,wp=Object(m["a"])(xp,_p,$p,!1,null,null,null);wp.options.__file="UserCreateDialog.vue";var Sp=wp.exports,Op=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){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)},Cp=[],Ep={mixins:[_],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("form/revert","users/"+t.user.id);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)})}}},jp=Ep,Tp=Object(m["a"])(jp,Op,Cp,!1,null,null,null);Tp.options.__file="UserEmailDialog.vue";var Ip=Tp.exports,Lp=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){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)},Ap=[],qp={mixins:[_],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.$store.state.user.current.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)})}}},Np=qp,Pp=Object(m["a"])(Np,Lp,Ap,!1,null,null,null);Pp.options.__file="UserLanguageDialog.vue";var Dp=Pp.exports,Bp=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){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)},Fp=[],Rp={mixins:[_],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.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)})}}},Mp=Rp,zp=Object(m["a"])(Mp,Bp,Fp,!1,null,null,null);zp.options.__file="UserPasswordDialog.vue";var Up=zp.exports,Hp=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)},Vp=[],Kp={mixins:[_],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("form/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)})}}},Gp=Kp,Yp=Object(m["a"])(Gp,Hp,Vp,!1,null,null,null);Yp.options.__file="UserRemoveDialog.vue";var Wp=Yp.exports,Jp=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){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)},Xp=[],Qp={mixins:[_],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.$api.users.changeName(this.user.id,this.user.name).then(function(){t.success({message:":)",event:"user.changeName"})}).catch(function(e){t.$refs.dialog.error(e.message)})}}},Zp=Qp,td=Object(m["a"])(Zp,Jp,Xp,!1,null,null,null);td.options.__file="UserRenameDialog.vue";var ed=td.exports,nd=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){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)},id=[],sd={mixins:[_],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().then(function(n){e.roles=n,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.success({message:":)",event:"user.changeRole"})}).catch(function(e){t.$refs.dialog.error(e.message)})}}},od=sd,ad=Object(m["a"])(od,nd,id,!1,null,null,null);ad.options.__file="UserRoleDialog.vue";var rd=ad.exports,ld=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){t.$emit("cancel")},submit:t.submit}},[t.issue?[n("k-box",{attrs:{text:t.issue,theme:"negative"}})]:[t.users.length?n("k-list",t._l(t.users,function(e,i){return n("k-list-item",{key:e.email,attrs:{text:e.username,image:e.avatar?{url:e.avatar.url,back:"pattern",cover:!0}:null,icon:{type:"user",back:"black"}},on:{click:function(e){t.toggle(i)}}},[e.selected?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-empty",{attrs:{icon:"users"}},[t._v("\n No users to select\n ")])]],2)},ud=[],cd={data:function(){return{users:[],issue:null,options:{max:null,multiple:!0,selected:[]}}},computed:{multiple:function(){return!0===this.options.multiple&&1!==this.options.max},checkedIcon:function(){return!0===this.multiple?"check":"circle-filled"}},methods:{fetch:function(){var t=this;return this.users=[],this.$api.get("users").then(function(e){var n=t.options.selected||[];t.users=e.data.map(function(t){return t.selected=-1!==n.indexOf(t.email),t})}).catch(function(e){t.users=[],t.issue=e.message})},selected:function(){return this.users.filter(function(t){return t.selected})},submit:function(){this.$emit("submit",this.selected()),this.$refs.dialog.close()},toggle:function(t){if(!1===this.options.multiple&&(this.users=this.users.map(function(t){return t.selected=!1,t})),this.users[t].selected)this.users[t].selected=!1;else{if(this.options.max&&this.options.max<=this.selected().length)return;this.users[t].selected=!0}},open:function(t){var e=this;this.options=t,this.fetch().then(function(){e.$refs.dialog.open()})}}},pd=cd,dd=(n("7568"),Object(m["a"])(pd,ld,ud,!1,null,null,null));dd.options.__file="UsersDialog.vue";var fd=dd.exports,hd=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.hasChanges?n("nav",{staticClass:"k-form-buttons"},[n("k-view",[n("k-button",{staticClass:"k-form-button",attrs:{icon:"undo"},on:{click:t.reset}},[t._v("\n "+t._s(t.$t("revert"))+"\n ")]),n("k-button",{staticClass:"k-form-button",attrs:{icon:"check"},on:{click:t.save}},[t._v("\n "+t._s(t.$t("save"))+"\n ")])],1)],1):t._e()},md=[],gd={computed:{hasChanges:function(){return this.$store.getters["form/hasChanges"](this.id)},id:function(){return this.$store.state.form.current}},created:function(){this.$events.$on("keydown.cmd.s",this.save)},destroyed:function(){this.$events.$off("keydown.cmd.s",this.save)},methods:{reset:function(){this.$store.dispatch("form/revert",this.id)},save:function(t){var e=this;return!!t&&(t.preventDefault&&t.preventDefault(),!1===this.hasChanges||void this.$store.dispatch("form/save",this.id).then(function(){e.$events.$emit("model.update"),e.$store.dispatch("notification/success",":)")}).catch(function(t){403!==t.code&&(t.details?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}]}))}))}}},vd=gd,bd=(n("18dd"),Object(m["a"])(vd,hd,md,!1,null,null,null));bd.options.__file="FormButtons.vue";var kd=bd.exports,_d=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)},$d=[],yd={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():t.dataTransfer.types?!1===t.dataTransfer.types.includes("Files")?this.reset():(this.$events.$emit("dropzone.drop"),this.files=t.dataTransfer.files,this.$emit("drop",this.files),void this.reset()):this.reset()},onEnter:function(t){!1===this.disabled&&t.dataTransfer.types&&t.dataTransfer.types.includes("Files")&&(this.dragging=!0)},onLeave:function(){this.reset()},onOver:function(t){!1===this.disabled&&t.dataTransfer.types&&t.dataTransfer.types.includes("Files")&&(t.dataTransfer.dropEffect="copy",this.over=!0)}}},xd=yd,wd=(n("414d"),Object(m["a"])(xd,_d,$d,!1,null,null,null));wd.options.__file="Dropzone.vue";var Sd=wd.exports,Od=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("a",{directives:[{name:"tab",rawName:"v-tab"}],staticClass:"k-file-preview-image-link",attrs:{href:t.file.url,title:t.$t("open"),target:"_blank"}},[t.file.panelImage&&t.file.panelImage.url?n("k-image",{attrs:{src:t.file.panelImage.url,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}}):t._e()],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)},Cd=[],Ed={props:{file:Object}},jd=Ed,Td=(n("696b"),Object(m["a"])(jd,Od,Cd,!1,null,null,null));Td.options.__file="FilePreview.vue";var Id=Td.exports,Ld=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){t.$emit("submit",e)}}}):t._e()},Ad=[],qd={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)}}}},Nd=qd,Pd=Object(m["a"])(Nd,Ld,Ad,!1,null,null,null);Pd.options.__file="Tabs.vue";var Dd=Pd.exports,Bd=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){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){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){t.change(e)}}},[t._v("\n "+t._s(e.name)+"\n ")])})],2):t._e()],1):t._e()},Fd=[],Rd={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)}}},Md=Rd,zd=Object(m["a"])(Md,Bd,Fd,!1,null,null,null);zd.options.__file="Languages.vue";var Ud=zd.exports,Hd=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){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.viewTitle(i,e))+"\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",{directives:[{name:"tab",rawName:"v-tab"}],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){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",{directives:[{name:"tab",rawName:"v-tab"}],key:"crumb-"+i,attrs:{to:e.link}},[t._v("\n "+t._s(e.label)+"\n ")])}),1),n("div",{staticClass:"k-topbar-signals"},[t.notification?n("k-button",{staticClass:"k-topbar-notification",attrs:{theme:"positive"},on:{click:function(e){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",{attrs:{responsive:!0,icon:"key"},on:{click:function(e){t.$emit("register")}}},[t._v(t._s(t.$t("license.register")))]),n("k-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-button",{attrs:{tooltip:t.$t("search"),icon:"search"},on:{click:function(e){t.$store.dispatch("search",!0)}}})],1)],1)])],1):t._e()},Vd=[],Kd=Object(u["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),Gd={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 Kd[this.$store.state.view]},views:function(){return Kd},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:{viewTitle:function(t,e){var n=this.$t("view.".concat(t),e.label);return"site"===t&&this.$store.state.system.info.breadcrumbTitle||n}}},Yd=Gd,Wd=(n("1e3b"),Object(m["a"])(Yd,Hd,Vd,!1,null,null,null));Wd.options.__file="Topbar.vue";var Jd=Wd.exports,Xd=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(e,s){return[t.exists(e.type)?n("k-"+e.type+"-section",t._b({key:t.parent+"-column-"+i+"-section-"+s+"-"+t.blueprint,tag:"component",class:"k-section k-section-name-"+e.name,attrs:{name:e.name,parent:t.parent,blueprint:t.blueprint},on:{submit:function(e){t.$emit("submit",e)}}},"component",e,!1)):[n("k-box",{key:t.parent+"-column-"+i+"-section-"+s,attrs:{text:t.$t("error.section.type.invalid",{type:e.type}),theme:"negative"}})]]})],2)}),1)},Qd=[],Zd={props:{parent:String,blueprint:String,columns:Array},methods:{exists:function(t){return i["a"].options.components["k-"+t+"-section"]}}},tf=Zd,ef=(n("6bcd"),Object(m["a"])(tf,Xd,Qd,!1,null,null,null));ef.options.__file="Sections.vue";var nf=ef.exports,sf=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)},of=[],af={props:{parent:String,blueprint:String,name:String},methods:{load:function(){return this.$api.get(this.parent+"/sections/"+this.name)}}},rf={mixins:[af],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})}},lf=rf,uf=(n("4333"),Object(m["a"])(lf,sf,of,!1,null,null,null));uf.options.__file="InfoSection.vue";var cf=uf.exports,pf=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:"This section is required"}},[t._v("*")]):t._e()]),t.add?n("k-button-group",[n("k-button",{attrs:{icon:"add"},on:{click:function(e){t.action(null,"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(t._s(t.$t("error.section.notLoaded",{name:t.name}))+":")]),t._v("\n "+t._s(t.error)+"\n ")])],1)]:[t.data.length?n("k-collection",{attrs:{layout:t.options.layout,items:t.data,pagination:t.pagination,sortable:t.options.sortable,size:t.options.size},on:{change:t.sort,paginate:t.paginate,action:t.action}}):n("k-empty",{attrs:{layout:t.options.layout,icon:"page"},on:{click:function(e){t.add&&t.action(null,"create")}}},[t._v("\n "+t._s(t.options.empty||t.$t("pages.empty"))+"\n ")]),n("k-page-create-dialog",{ref:"create"}),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()},df=[],ff={props:{parent:String,blueprint:String,name:String},data:function(){return{data:[],error:null,isLoading:!1,options:{empty:null,headline:null,layout:"list",link:null,max:null,min:null,size:null,sortable:null},pagination:{page:null}}},computed:{headline:function(){return this.options.headline||" "},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)}}},hf={mixins:[ff],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:{action:function(t,e){var n=this;switch(e){case"create":this.$refs.create.open(this.options.link||this.parent,this.parent+"/children/blueprints",this.name);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":this.$refs.remove.open(t.id);break;default:throw new Error("Invalid action")}},items:function(t){var e=this;return t.map(function(t){return t.flag={class:"k-status-flag k-status-flag-"+t.status,tooltip:e.$t("page.status"),icon:!1===t.permissions.changeStatus?"protected":"circle",disabled:!1===t.permissions.changeStatus,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})},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")}}},mf=hf,gf=Object(m["a"])(mf,pf,df,!1,null,null,null);gf.options.__file="PagesSection.vue";var vf=gf.exports,bf=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:"This section is 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:{items:t.data,layout:t.options.layout,pagination:t.pagination,sortable:t.options.sortable,size:t.options.size},on:{sort:t.sort,paginate:t.paginate,action:t.action}}):n("k-empty",{attrs:{layout:t.options.layout,icon:"image"},on:{click:function(e){t.add&&t.upload()}}},[t._v("\n "+t._s(t.options.empty||t.$t("files.empty"))+"\n ")])],1),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()},kf=[],_f={mixins:[ff],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.replace(t);break;case"remove":this.$refs.remove.open(t.parent,t.filename);break}},drop:function(t){if(!1===this.add)return!1;this.$refs.upload.drop(t,Object(u["a"])({},this.add,{url:d.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})},replace:function(t){this.$refs.upload.open({url:d.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}).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(u["a"])({},this.add,{url:d.api+"/"+this.add.api}))},uploaded:function(){this.$events.$emit("file.create"),this.$events.$emit("model.update"),this.$store.dispatch("notification/success",":)")}}},$f=_f,yf=Object(m["a"])($f,bf,kf,!1,null,null,null);yf.options.__file="FilesSection.vue";var xf=yf.exports,wf=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},on:{input:t.input,submit:t.onSubmit}})],2)},Sf=[],Of={mixins:[af],data:function(){return{fields:{},isLoading:!0,issue:null}},computed:{id:function(){return this.$store.state.form.current},language:function(){return this.$store.state.languages.current},values:function(){return this.$store.getters["form/values"](this.id)}},watch:{$route:function(){this.fields={},this.isLoading=!0,this.issue=null},language:function(){this.fetch()}},created:function(){this.fetch()},methods:{input:function(t,e,n){this.$store.dispatch("form/update",[this.id,n,t[n]])},fetch:function(){var t=this;this.$api.get(this.parent+"/sections/"+this.name).then(function(e){t.fields=e.fields,Object.keys(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)}}},Cf=Of,Ef=(n("7d5d"),Object(m["a"])(Cf,wf,Sf,!1,null,null,null));Ef.options.__file="FieldsSection.vue";var jf=Ef.exports,Tf=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)])},If=[],Lf=(n("d221"),{}),Af=Object(m["a"])(Lf,Tf,If,!1,null,null,null);Af.options.__file="ErrorView.vue";var qf=Af.exports;i["a"].component("k-dialog",qu),i["a"].component("k-error-dialog",Ru),i["a"].component("k-file-rename-dialog",ec),i["a"].component("k-file-remove-dialog",Ku),i["a"].component("k-files-dialog",rc),i["a"].component("k-language-create-dialog",fc),i["a"].component("k-language-remove-dialog",kc),i["a"].component("k-language-update-dialog",Sc),i["a"].component("k-page-create-dialog",Ic),i["a"].component("k-page-rename-dialog",Uc),i["a"].component("k-page-remove-dialog",Dc),i["a"].component("k-page-status-dialog",Wc),i["a"].component("k-page-template-dialog",ep),i["a"].component("k-page-url-dialog",rp),i["a"].component("k-pages-dialog",mp),i["a"].component("k-site-rename-dialog",kp),i["a"].component("k-user-create-dialog",Sp),i["a"].component("k-user-email-dialog",Ip),i["a"].component("k-user-language-dialog",Dp),i["a"].component("k-user-password-dialog",Up),i["a"].component("k-user-remove-dialog",Wp),i["a"].component("k-user-rename-dialog",ed),i["a"].component("k-user-role-dialog",rd),i["a"].component("k-users-dialog",fd),i["a"].component("k-form-buttons",kd),i["a"].component("k-dropzone",Sd),i["a"].component("k-file-preview",Id),i["a"].component("k-tabs",Dd),i["a"].component("k-languages-dropdown",Ud),i["a"].component("k-topbar",Jd),i["a"].component("k-sections",nf),i["a"].component("k-info-section",cf),i["a"].component("k-pages-section",vf),i["a"].component("k-files-section",xf),i["a"].component("k-fields-section",jf),i["a"].component("k-error-view",qf);var Nf={user:function(){return dm.get("auth")},login:function(t){var e={long:t.remember||!1,email:t.email,password:t.password};return dm.post("auth/login",e).then(function(t){return t.user})},logout:function(){return dm.post("auth/logout")}},Pf={get:function(t,e,n){return dm.get(this.url(t,e),n).then(function(t){return!0===Array.isArray(t.content)&&(t.content={}),t})},update:function(t,e,n){return dm.patch(this.url(t,e),n)},rename:function(t,e,n){return dm.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 dm.delete(this.url(t,e))},options:function(t,e,n){return dm.get(this.url(t,e),{select:"options"}).then(function(t){var e=t.options,s=[];return"list"===n&&s.push({icon:"open",text:i["a"].i18n.translate("open"),click:"download"}),s.push({icon:"title",text:i["a"].i18n.translate("rename"),click:"rename",disabled:!e.changeName}),s.push({icon:"upload",text:i["a"].i18n.translate("replace"),click:"replace",disabled:!e.replace}),s.push({icon:"trash",text:i["a"].i18n.translate("delete"),click:"remove",disabled:!e.delete}),s})},breadcrumb:function(t,e){var n=null,i=[];switch(e){case"UserFile":i.push({label:t.parent.username,link:dm.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:dm.pages.link(t.id)}}),n=dm.pages.url(t.parent.id);break}return i.push({label:t.filename,link:this.link(n,t.filename)}),i}},Df={create:function(t,e){return null===t||"/"===t?dm.post("site/children",e):dm.post(this.url(t,"children"),e)},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 dm.get(this.url(t),e).then(function(t){return!0===Array.isArray(t.content)&&(t.content={}),t})},options:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"view";return dm.get(this.url(t),{select:"options"}).then(function(t){var n=t.options,s=[];return"list"===e&&s.push({click:"preview",icon:"open",text:i["a"].i18n.translate("open"),disabled:!1===n.preview}),s.push({click:"rename",icon:"title",text:i["a"].i18n.translate("rename"),disabled:!n.changeTitle}),s.push({click:"url",icon:"url",text:i["a"].i18n.translate("page.changeSlug"),disabled:!n.changeSlug}),s.push({click:"status",icon:"preview",text:i["a"].i18n.translate("page.changeStatus"),disabled:!n.changeStatus}),s.push({click:"template",icon:"template",text:i["a"].i18n.translate("page.changeTemplate"),disabled:!n.changeTemplate}),s.push({click:"remove",icon:"trash",text:i["a"].i18n.translate("delete"),disabled:!n.delete}),s})},preview:function(t){return this.get(t,{select:"previewUrl"}).then(function(t){return t.previewUrl})},update:function(t,e){return dm.patch(this.url(t),e)},children:function(t,e){return dm.post(this.url(t,"children/search"),e)},files:function(t,e){return dm.post(this.url(t,"files/search"),e)},delete:function(t,e){return dm.delete(this.url(t),e)},slug:function(t,e){return dm.patch(this.url(t,"slug"),{slug:e})},title:function(t,e){return dm.patch(this.url(t,"title"),{title:e})},template:function(t,e){return dm.patch(this.url(t,"template"),{template:e})},search:function(t,e){return t?dm.post("pages/"+t.replace("/","+")+"/children/search?select=id,title,hasChildren",e):dm.post("site/children/search?select=id,title,hasChildren",e)},status:function(t,e,n){return dm.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}},Bf=n("2f62"),Ff=n("3835"),Rf={namespaced:!0,state:{models:{},current:null,isLocked:!1},getters:{current:function(t){return t.current},exists:function(t){return function(e){return t.models.hasOwnProperty(e)}},hasChanges:function(t,e){return function(t){return Object.keys(e.model(t).changes).length>0}},id:function(t,e,n){return function(t){return n.languages.current?t+"/"+n.languages.current.code:t}},isCurrent:function(t){return function(e){return t.current=e}},model:function(t,e){return function(n){return e.exists(n)?t.models[n]:{originals:{},values:{},changes:{},api:null}}},originals:function(t,e){return function(t){return yr(e.model(t).originals)}},values:function(t,e){return function(t){return yr(e.model(t).values)}}},mutations:{CREATE:function(t,e){i["a"].set(t.models,e.id,{api:e.api,originals:yr(e.content),values:yr(e.content),changes:{}})},CURRENT:function(t,e){t.current=e},IS_LOCKED:function(t,e){t.isLocked=e},REMOVE:function(t,e){i["a"].delete(t.models,e),localStorage.removeItem("kirby$form$"+e)},DELETE_CHANGES:function(t,e){i["a"].set(t.models[e],"changes",{}),localStorage.removeItem("kirby$form$"+e)},SET_ORIGINALS:function(t,e){var n=Object(Ff["a"])(e,2),i=n[0],s=n[1];t.models[i].originals=yr(s)},SET_VALUES:function(t,e){var n=Object(Ff["a"])(e,2),i=n[0],s=n[1];t.models[i].values=yr(s)},UPDATE:function(t,e){var n=Object(Ff["a"])(e,3),s=n[0],o=n[1],a=n[2];a=yr(a),i["a"].set(t.models[s].values,o,a);var r=JSON.stringify(t.models[s].originals[o]),l=JSON.stringify(a);r===l?i["a"].delete(t.models[s].changes,o):i["a"].set(t.models[s].changes,o,!0),localStorage.setItem("kirby$form$"+s,JSON.stringify(t.models[s].values))}},actions:{create:function(t,e){t.rootState.languages.current&&t.rootState.languages.current.code&&(e.id=t.getters.id(e.id)),t.commit("CREATE",e),t.commit("CURRENT",e.id);var n=localStorage.getItem("kirby$form$"+e.id);if(n){var i=JSON.parse(n);Object.keys(i).forEach(function(n){var s=i[n];t.commit("UPDATE",[e.id,n,s])})}},remove:function(t,e){t.commit("REMOVE",e)},revert:function(t,e){var n=t.getters.model(e);return dm.get(n.api,{select:"content"}).then(function(n){t.commit("SET_ORIGINALS",[e,n.content]),t.commit("SET_VALUES",[e,n.content]),t.commit("DELETE_CHANGES",e)})},save:function(t,e){e=e||t.state.current;var n=t.getters.model(e);return(!t.getters.isCurrent(e)||!t.state.isLocked)&&dm.patch(n.api,n.values).then(function(){t.dispatch("revert",e)})},lock:function(t){t.commit("IS_LOCKED",!0)},unlock:function(t){t.commit("IS_LOCKED",!1)},update:function(t,e){var n=Object(Ff["a"])(e,3),i=n[0],s=n[1],o=n[2];t.commit("UPDATE",[i,s,o])}}},Mf={namespaced:!0,state:{all:[],current:null,default:null},mutations:{SET_ALL:function(t,e){t.all=e.map(function(t){return{code:t.code,name:t.name,default:t.default,direction:t.direction}})},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.commit("SET_CURRENT",s)}t.commit("SET_CURRENT",n||e[0])},load:function(t){return dm.get("languages").then(function(e){t.dispatch("install",e.data)})}}},zf={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(u["a"])({type:"success",timeout:4e3},e))},error:function(t,e){"string"===typeof e&&(e={message:e}),t.dispatch("open",Object(u["a"])({type:"error"},e))}}},Uf={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 Promise(function(e){e(t.state.info)}):dm.system.info({view:"panel"}).then(function(e){return t.commit("SET_INFO",Object(u["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})})}}},Hf={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 dm.translations.get(e)},install:function(t,e){t.commit("INSTALL",e),i["a"].i18n.add(e.id,e.data)},activate:function(t,e){var n=t.state.installed[e];n?(i["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)})}}},Vf=n("8c4f"),Kf=function(t,e,n){om.dispatch("system/load").then(function(){var e=om.state.user.current;if(!e)return om.dispatch("user/visit",t.path),om.dispatch("user/logout"),!1;var s=e.permissions.access;return!1===s.panel?(window.location.href=d.site,!1):!1===s[t.meta.view]?(om.dispatch("notification/error",{message:i["a"].i18n.translate("error.access.view")}),n("/")):void n()})},Gf=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)},Yf=[],Wf={grid:function(){return!(!window.CSS||!window.CSS.supports("display","grid"))},fetch:function(){return void 0!==window.fetch},all:function(){return this.fetch()&&this.grid()}},Jf={computed:{hasFetchSupport:function(){return Wf.fetch()},hasGridSupport:function(){return Wf.grid()}},created:function(){Wf.all()&&this.$router.push("/")}},Xf=Jf,Qf=(n("d6fc"),Object(m["a"])(Xf,Gf,Yf,!1,null,null,null));Qf.options.__file="BrowserView.vue";var Zf=Qf.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)+"\n ")])}}])},[n("k-"+t.plugin+"-plugin-view",{tag:"component"})],1)},eh=[],nh={props:{plugin:String},watch:{plugin:function(){this.$store.dispatch("view",this.plugin)}},created:function(){this.$store.dispatch("view",this.plugin)}},ih=nh,sh=Object(m["a"])(ih,th,eh,!1,null,null,null);sh.options.__file="CustomView.vue";var oh=sh.exports,ah=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"},[n("k-header",{attrs:{editable:t.permissions.changeName,tabs:t.tabs,tab:t.tab},on:{edit:function(e){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){t.action("download")}}},[t._v("\n "+t._s(t.$t("open"))+"\n ")]),n("k-dropdown",[n("k-button",{attrs:{responsive:!0,icon:"cog"},on:{click:function(e){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:"file-"+t.file.id+"-tabs",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)},rh=[],lh={created:function(){this.fetch(),this.$events.$on("keydown.left",this.toPrev),this.$events.$on("keydown.right",this.toNext)},destroyed:function(){this.$events.$off("keydown.left",this.toPrev),this.$events.$off("keydown.right",this.toNext)},watch:{$route:function(){this.fetch()}},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)}}},uh={mixins:[lh],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 d.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}},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()},path: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("form/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:d.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",":)")}}},ch=uh,ph=Object(m["a"])(ch,ah,rh,!1,null,null,null);ph.options.__file="FileView.vue";var dh=ph.exports,fh=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"}},[t.system.isOk&&!t.system.isInstalled?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):t.system.isOk&&t.system.isInstalled?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()},hh=[],mh={data:function(){return{user:{name:"",email:"",language:"en",password:"",role:"admin"},languages:[],system:null}},computed:{translation:function(){return this.$store.state.translation.current},requirements:function(){return this.system&&this.system.requirements?this.system.requirements:{}},fields:function(){return{name:{label:this.$t("name"),type:"text",icon:"user",autofocus:!0},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:function(t){this.user.language=t},"user.language":function(t){this.$store.dispatch("translation/activate",t)}},created:function(){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"))})})}}},gh=mh,vh=(n("146c"),Object(m["a"])(gh,fh,hh,!1,null,null,null));vh.options.__file="InstallationView.vue";var bh=vh.exports,kh=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){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){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){t.$refs.create.open()}}},[t._v(t._s(t.$t("language.create")))])],1),n("k-empty",{attrs:{icon:"globe"},on:{click:function(e){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)},_h=[],$h={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.fetch(),this.$store.dispatch("title",this.$t("view.settings")),this.$store.dispatch("breadcrumb",[])},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,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}}}},yh=$h,xh=(n("9bd5"),Object(m["a"])(yh,kh,_h,!1,null,null,null));xh.options.__file="SettingsView.vue";var wh=xh.exports,Sh=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("form",{staticClass:"k-login-form",attrs:{"data-invalid":t.invalid},on:{submit:function(e){return e.preventDefault(),t.login(e)}}},[n("h1",{staticClass:"k-offscreen"},[t._v(t._s(t.$t("login")))]),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)]):t._e()},Oh=[],Ch={data:function(){return{ready:!1,issue:null,invalid:!1,isLoading:!1,user:{email:"",password:"",remember:!1}}},computed:{fields:function(){return{email:{autofocus:!0,label:this.$t("email"),type:"email",link:!1},password:{label:this.$t("password"),type:"password",minLength:8,autocomplete:"current-password",counter:!1}}}},created:function(){var t=this;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})},methods:{login:function(){var t=this;this.invalid=!1,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.invalid=!0,t.isLoading=!1})}}},Eh=Ch,jh=(n("24c1"),Object(m["a"])(Eh,Sh,Oh,!1,null,null,null));jh.options.__file="LoginView.vue";var Th=jh.exports,Ih=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"},[n("k-header",{attrs:{tabs:t.tabs,tab:t.tab,editable:t.permissions.changeTitle},on:{edit:function(e){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:!1===t.permissions.changeStatus,icon:!1===t.permissions.changeStatus?"protected":"circle",responsive:!0},on:{click:function(e){t.action("status")}}},[t._v("\n "+t._s(t.status.label)+"\n ")]):t._e(),n("k-dropdown",[n("k-button",{attrs:{responsive:!0,icon:"cog"},on:{click:function(e){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:function(e){t.tab=e}}}):t._e(),n("k-page-rename-dialog",{ref:"rename",on:{success:t.update}}),n("k-page-url-dialog",{ref:"url",on:{success:function(e){t.$emit("model.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"})],1)},Lh=[],Ah={mixins:[lh],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:{prev:function(){if(this.page.prev)return{link:this.$api.pages.link(this.page.prev.id),tooltip:this.page.prev.title}},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}},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()}},methods:{action:function(t){var e=this;switch(t){case"preview":this.$api.pages.preview(this.page.id).then(function(t){window.open(t)}).catch(function(t){e.$store.dispatch("notification/error",t)});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}},changeLanguage:function(t){this.$store.dispatch("languages/current",t),this.fetch()},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("form/create",{id:"pages/"+e.id,api:t.$api.pages.link(e.id),content:e.content})}).catch(function(e){t.issue=e})},update:function(){this.fetch(),this.$emit("model.update")}}},qh=Ah,Nh=(n("202d"),Object(m["a"])(qh,Ih,Lh,!1,null,null,null));Nh.options.__file="PageView.vue";var Ph=Nh.exports,Dh=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"},[n("k-header",{attrs:{tabs:t.tabs,tab:t.tab,editable:t.permissions.changeTitle},on:{edit:function(e){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.url,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)},Bh=[],Fh={data:function(){return{site:{title:null,url:null},issue:null,tab:null,tabs:[],options:null,permissions:{changeTitle:!0}}},computed:{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("form/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}}}},Rh=Fh,Mh=Object(m["a"])(Rh,Dh,Bh,!1,null,null,null);Mh.options.__file="SiteView.vue";var zh=Mh.exports,Uh=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){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){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){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){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)},Hh=[],Vh={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.$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}};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()}}},Kh=Vh,Gh=Object(m["a"])(Kh,Uh,Hh,!1,null,null,null);Gh.options.__file="UsersView.vue";var Yh=Gh.exports,Wh=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"},[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")},on:{click:function(e){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){t.$refs.upload.open()}}},[t._v("\n "+t._s(t.$t("change"))+"\n ")]),n("k-dropdown-item",{attrs:{icon:"trash"},on:{click:function(e){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){t.$refs.upload.open()}}},[n("k-icon",{attrs:{type:"user"}})],1)],n("k-button-group",[n("k-button",{attrs:{disabled:!t.permissions.changeEmail,icon:"email"},on:{click:function(e){t.action("email")}}},[t._v(t._s(t.$t("email"))+": "+t._s(t.user.email))]),n("k-button",{attrs:{disabled:!t.permissions.changeRole,icon:"bolt"},on:{click:function(e){t.action("role")}}},[t._v(t._s(t.$t("role"))+": "+t._s(t.user.role.title))]),n("k-button",{attrs:{disabled:!t.permissions.changeLanguage,icon:"globe"},on:{click:function(e){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,tabs:t.tabs,tab:t.tab},on:{edit:function(e){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:{icon:"cog"},on:{click:function(e){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:"user-"+t.user.id+"-tabs-"+(new Date).getTime(),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()},Jh=[],Xh={mixins:[lh],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}},uploadApi:function(){return d.api+"/users/"+this.user.id+"/avatar"}},watch:{language: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("form/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()}}},Qh=Xh,Zh=(n("bd96"),Object(m["a"])(Qh,Wh,Jh,!1,null,null,null));Zh.options.__file="UserView.vue";var tm=Zh.exports,em=[{path:"/",name:"Home",redirect:"/site"},{path:"/browser",name:"Browser",component:Zf,meta:{outside:!0}},{path:"/login",component:Th,meta:{outside:!0}},{path:"/logout",beforeEnter:function(){om.dispatch("user/logout")},meta:{outside:!0}},{path:"/installation",component:bh,meta:{outside:!0}},{path:"/site",name:"Site",meta:{view:"site"},component:zh,beforeEnter:Kf},{path:"/site/files/:filename",name:"SiteFile",meta:{view:"site"},component:dh,beforeEnter:Kf,props:function(t){return{path:"site",filename:t.params.filename}}},{path:"/pages/:path/files/:filename",name:"PageFile",meta:{view:"site"},component:dh,beforeEnter:Kf,props:function(t){return{path:"pages/"+t.params.path,filename:t.params.filename}}},{path:"/users/:path/files/:filename",name:"UserFile",meta:{view:"users"},component:dh,beforeEnter:Kf,props:function(t){return{path:"users/"+t.params.path,filename:t.params.filename}}},{path:"/pages/:path",name:"Page",meta:{view:"site"},component:Ph,beforeEnter:Kf,props:function(t){return{path:t.params.path}}},{path:"/settings",name:"Settings",meta:{view:"settings"},component:wh,beforeEnter:Kf},{path:"/users/role/:role",name:"UsersByRole",meta:{view:"users"},component:Yh,beforeEnter:Kf,props:function(t){return{role:t.params.role}}},{path:"/users",name:"Users",meta:{view:"users"},beforeEnter:Kf,component:Yh},{path:"/users/:id",name:"User",meta:{view:"users"},component:tm,beforeEnter:Kf,props:function(t){return{id:t.params.id}}},{path:"/account",name:"Account",meta:{view:"account"},component:tm,beforeEnter:Kf,props:function(){return{id:om.state.user.current.id}}},{path:"/plugins/:id",name:"Plugin",meta:{view:"plugin"},props:function(t){return{plugin:t.params.id}},beforeEnter:Kf,component:oh},{path:"*",name:"NotFound",beforeEnter:function(t,e,n){n("/")}}];i["a"].use(Vf["a"]);var nm=new Vf["a"]({mode:"history",routes:em,url:"/"===d.url?"":d.url});nm.beforeEach(function(t,e,n){"Browser"!==t.name&&!1===Wf.all()&&n("/browser"),om.dispatch("view",t.meta.view),t.meta.outside||om.dispatch("user/visit",t.path),n()});var im=nm,sm={namespaced:!0,state:{current:null,path:null},mutations:{SET_CURRENT:function(t,e){t.current=e,e&&e.permissions?(i["a"].prototype.$user=e,i["a"].prototype.$permissions=e.permissions):(i["a"].prototype.$user=null,i["a"].prototype.$permissions=null)},SET_PATH:function(t,e){t.path=e}},actions:{current:function(t,e){t.commit("SET_CURRENT",e)},language:function(t,e){t.dispatch("translation/activate",e,{root:!0}),t.commit("SET_CURRENT",Object(u["a"])({language:e},t.state.current))},load:function(t){return dm.auth.user().then(function(e){return t.commit("SET_CURRENT",e),e})},login:function(t,e){return dm.auth.login(e).then(function(e){return t.commit("SET_CURRENT",e),t.dispatch("translation/activate",e.language,{root:!0}),im.push(t.state.path||"/"),e})},logout:function(t,e){t.commit("SET_CURRENT",null),e?window.location.href=(window.panel.url||"")+"/login":dm.auth.logout().then(function(){im.push("/login")}).catch(function(){im.push("/login")})},visit:function(t,e){t.commit("SET_PATH",e)}}};i["a"].use(Bf["a"]);var om=new Bf["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:{form:Rf,languages:Mf,notification:zf,system:Uf,translation:Hf,user:sm}}),am={running:0,request:function(t,e){var n=this;return e=Object.assign(e||{},{credentials:"same-origin",headers:Object(u["a"])({"x-requested-with":"xmlhttprequest","content-type":"application/json"},e.headers)}),om.state.languages.current&&(e.headers["x-language"]=om.state.languages.current.code),e.headers["x-csrf"]=window.panel.csrf,dm.config.onStart(),this.running++,fetch(dm.config.endpoint+"/"+t,e).then(function(t){return t.json()}).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--,dm.config.onComplete(),dm.config.onSuccess(t),e}).catch(function(t){throw n.running--,dm.config.onComplete(),dm.config.onError(t),t})},get:function(t,e,n){return e&&(t+="?"+Object.keys(e).map(function(t){return t+"="+e[t]}).join("&")),this.request(t,Object.assign(n||{},{method:"GET"}))},post:function(t,e,n){var i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"POST";return this.request(t,Object.assign(n||{},{method:i,body:JSON.stringify(e)}))},patch:function(t,e,n){return this.post(t,e,n,"PATCH")},delete:function(t,e,n){return this.post(t,e,n,"DELETE")}},rm={list:function(){return dm.get("roles")},get:function(t){return dm.get("roles/"+t)},options:function(){return this.list().then(function(t){return t.data.map(function(t){return{info:t.description||"(".concat(i["a"].i18n.translate("role.description.placeholder"),")"),text:t.title,value:t.name}})})}},lm={info:function(t){return dm.get("system",t)},install:function(t){return dm.post("system/install",t).then(function(t){return t.user})},register:function(t){return dm.post("system/register",t)}},um={get:function(t){return dm.get("site",t)},update:function(t){return dm.post("site",t)},title:function(t){return dm.patch("site/title",{title:t})},options:function(){return dm.get("site",{select:"options"}).then(function(t){var e=t.options,n=[];return n.push({click:"rename",icon:"title",text:i["a"].i18n.translate("rename"),disabled:!e.changeTitle}),n})},children:function(t){return dm.post("site/children/search",t)},blueprint:function(){return dm.get("site/blueprint")},blueprints:function(){return dm.get("site/blueprints")}},cm={list:function(){return dm.get("translations")},get:function(t){return dm.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})}},pm={create:function(t){return dm.post(this.url(),t)},list:function(t){return dm.post(this.url(null,"search"),t)},get:function(t,e){return dm.get(this.url(t),e)},update:function(t,e){return dm.patch(this.url(t),e)},delete:function(t){return dm.delete(this.url(t))},changeEmail:function(t,e){return dm.patch(this.url(t,"email"),{email:e})},changeLanguage:function(t,e){return dm.patch(this.url(t,"language"),{language:e})},changeName:function(t,e){return dm.patch(this.url(t,"name"),{name:e})},changePassword:function(t,e){return dm.patch(this.url(t,"password"),{password:e})},changeRole:function(t,e){return dm.patch(this.url(t,"role"),{role:e})},deleteAvatar:function(t){return dm.delete(this.url(t,"avatar"))},blueprint:function(t){return dm.get(this.url(t,"blueprint"))},breadcrumb:function(t){return[{link:"/users/"+t.id,label:t.username}]},options:function(t){return dm.get(this.url(t),{select:"options"}).then(function(t){var e=t.options,n=[];return n.push({click:"rename",icon:"title",text:i["a"].i18n.translate("user.changeName"),disabled:!e.changeName}),n.push({click:"email",icon:"email",text:i["a"].i18n.translate("user.changeEmail"),disabled:!e.changeEmail}),n.push({click:"role",icon:"bolt",text:i["a"].i18n.translate("user.changeRole"),disabled:!e.changeRole}),n.push({click:"password",icon:"key",text:i["a"].i18n.translate("user.changePassword"),disabled:!e.changePassword}),n.push({click:"language",icon:"globe",text:i["a"].i18n.translate("user.changeLanguage"),disabled:!e.changeLanguage}),n.push({click:"remove",icon:"trash",text:i["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)}},dm=Object(u["a"])({config:{onStart:function(){},onComplete:function(){},onSuccess:function(){},onError:function(t){throw window.console.log(t.message),t}},auth:Nf,files:Pf,pages:Df,roles:rm,system:lm,site:um,translations:cm,users:pm},am);dm.config.endpoint=d.api,dm.config.onStart=function(){om.dispatch("isLoading",!0)},dm.config.onComplete=function(){om.dispatch("isLoading",!1)},dm.config.onError=function(t){d.debug&&window.console.error(t),403===t.code&&om.dispatch("user/logout",!0)};var fm=setInterval(dm.auth.user,3e5);dm.config.onSuccess=function(){clearInterval(fm),fm=setInterval(dm.auth.user,3e5)},i["a"].prototype.$api=dm,i["a"].config.errorHandler=function(t){d.debug&&window.console.error(t),om.dispatch("notification/error",{message:t.message||"An error occurred. Please reload the panel"})},window.panel=window.panel||{},window.panel.error=function(t,e){d.debug&&window.console.error(t+": "+e),om.dispatch("error",t+". See the console for more information.")};var hm=n("f2f3");i["a"].use(hm["a"].plugin,om);n("ffc1");var mm={};for(var gm in i["a"].options.components)mm[gm]=i["a"].options.components[gm];var vm=function(t,e){e.template||e.render||e.extends?(e.extends&&"string"===typeof e.extends&&(e.extends=mm[e.extends],e.template&&(e.render=null)),e.mixins&&(e.mixins=e.mixins.map(function(t){return"string"===typeof t?mm[t]:t})),mm[t]&&window.console.warn('Plugin is replacing "'.concat(t,'"')),i["a"].component(t,e)):om.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.'))};Object.entries(window.panel.plugins.components).forEach(function(t){var e=Object(Ff["a"])(t,2),n=e[0],i=e[1];vm(n,i)}),Object.entries(window.panel.plugins.fields).forEach(function(t){var e=Object(Ff["a"])(t,2),n=e[0],i=e[1];vm(n,i)}),Object.entries(window.panel.plugins.sections).forEach(function(t){var e=Object(Ff["a"])(t,2),n=e[0],i=e[1];vm(n,Object(u["a"])({},i,{mixins:[af].concat(i.mixins||[])}))}),Object.entries(window.panel.plugins.views).forEach(function(t){var e=Object(Ff["a"])(t,2),n=e[0],s=e[1];if(!s.component)return om.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];s.link="/plugins/"+n,void 0===s.icon&&(s.icon="page"),void 0===s.menu&&(s.menu=!0),window.panel.plugins.views[n]={link:s.link,icon:s.icon,menu:s.menu},i["a"].component("k-"+n+"-plugin-view",s.component)}),window.panel.plugins.use.forEach(function(t){i["a"].use(t)}),i["a"].config.productionTip=!1,i["a"].config.devtools=!0,window.panel.app=new i["a"]({router:im,store:om,render:function(t){return t(E)}}).$mount("#app")},5714:function(t,e,n){},"58e5":function(t,e,n){},"5c0b":function(t,e,n){"use strict";var i=n("5e27"),s=n.n(i);s.a},"5e27":function(t,e,n){},"5e3a":function(t,e,n){"use strict";var i=n("7bb1"),s=n.n(i);s.a},"5f4f":function(t,e,n){},"5f5b":function(t,e,n){"use strict";var i=n("8915"),s=n.n(i);s.a},6022:function(t,e,n){"use strict";var i=n("b31f"),s=n.n(i);s.a},"622c":function(t,e,n){},"64e6":function(t,e,n){},"68b5":function(t,e,n){"use strict";var i=n("d2f5"),s=n.n(i);s.a},6937:function(t,e,n){},"696b":function(t,e,n){"use strict";var i=n("0cdc"),s=n.n(i);s.a},"6a0a":function(t,e,n){"use strict";var i=n("5439"),s=n.n(i);s.a},"6ab9":function(t,e,n){},"6af3":function(t,e,n){},"6b18":function(t,e,n){},"6b7f":function(t,e,n){},"6b96":function(t,e,n){},"6bcd":function(t,e,n){"use strict";var i=n("9e0a"),s=n.n(i);s.a},"6d8c":function(t,e,n){},7027:function(t,e,n){"use strict";var i=n("cd7a"),s=n.n(i);s.a},7075:function(t,e,n){},7428:function(t,e,n){},7568:function(t,e,n){"use strict";var i=n("4150"),s=n.n(i);s.a},"75ae":function(t,e,n){},7737:function(t,e,n){"use strict";var i=n("ca19"),s=n.n(i);s.a},"77f7":function(t,e,n){"use strict";var i=n("200b"),s=n.n(i);s.a},"791b":function(t,e,n){"use strict";var i=n("ea0f"),s=n.n(i);s.a},"7bb1":function(t,e,n){},"7d5d":function(t,e,n){"use strict";var i=n("6ab9"),s=n.n(i);s.a},"813c":function(t,e,n){"use strict";var i=n("c664"),s=n.n(i);s.a},8633:function(t,e,n){"use strict";var i=n("3755"),s=n.n(i);s.a},8915:function(t,e,n){},"891e":function(t,e,n){},"8ae6":function(t,e,n){},"8b1d":function(t,e,n){},"8be2":function(t,e,n){"use strict";var i=n("e0b0"),s=n.n(i);s.a},"8e2d":function(t,e,n){"use strict";var i=n("6d8c"),s=n.n(i);s.a},"988f":function(t,e,n){"use strict";var i=n("ea9f"),s=n.n(i);s.a},"9adb":function(t,e,n){},"9ae6":function(t,e,n){},"9bd5":function(t,e,n){"use strict";var i=n("64e6"),s=n.n(i);s.a},"9df9":function(t,e,n){},"9e0a":function(t,e,n){},"9ee7":function(t,e,n){},a2a8:function(t,e,n){"use strict";var i=n("6937"),s=n.n(i);s.a},a319:function(t,e,n){},a361:function(t,e,n){"use strict";var i=n("9adb"),s=n.n(i);s.a},a89c:function(t,e,n){"use strict";var i=n("acc9"),s=n.n(i);s.a},aa8b:function(t,e,n){"use strict";var i=n("b5db"),s=n.n(i);s.a},ac27:function(t,e,n){"use strict";var i=n("3c9d"),s=n.n(i);s.a},acc9:function(t,e,n){},b2d3:function(t,e,n){},b31f:function(t,e,n){},b42a:function(t,e,n){"use strict";var i=n("a319"),s=n.n(i);s.a},b5db:function(t,e,n){},b61e:function(t,e,n){"use strict";var i=n("d268"),s=n.n(i);s.a},b83b:function(t,e,n){"use strict";var i=n("9df9"),s=n.n(i);s.a},b8aa:function(t,e,n){},b8aa9:function(t,e,n){"use strict";var i=n("c9df"),s=n.n(i);s.a},bbbf:function(t,e,n){},bd46:function(t,e,n){"use strict";var i=n("f01a"),s=n.n(i);s.a},bd6e:function(t,e,n){"use strict";var i=n("3218"),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},c245:function(t,e,n){},c664:function(t,e,n){},c9df:function(t,e,n){},ca19:function(t,e,n){},ca3a:function(t,e,n){},cd7a:function(t,e,n){},d11d:function(t,e,n){"use strict";var i=n("0812"),s=n.n(i);s.a},d221:function(t,e,n){"use strict";var i=n("6b7f"),s=n.n(i);s.a},d268:function(t,e,n){},d2f5:function(t,e,n){},d4da:function(t,e,n){},d6a4:function(t,e,n){},d6f2:function(t,e,n){},d6fc:function(t,e,n){"use strict";var i=n("08ec"),s=n.n(i);s.a},dccd:function(t,e,n){},dd48:function(t,e,n){},dea4:function(t,e,n){"use strict";var i=n("dd48"),s=n.n(i);s.a},df30:function(t,e,n){"use strict";var i=n("28f4"),s=n.n(i);s.a},e0b0:function(t,e,n){},e104:function(t,e,n){"use strict";var i=n("6b18"),s=n.n(i);s.a},e697:function(t,e,n){},ea0f:function(t,e,n){},ea9f:function(t,e,n){},eabd:function(t,e,n){"use strict";var i=n("b2d3"),s=n.n(i);s.a},ec72:function(t,e,n){},f01a:function(t,e,n){},f093:function(t,e,n){"use strict";var i=n("2114"),s=n.n(i);s.a},f09b:function(t,e,n){},f32d:function(t,e,n){"use strict";var i=n("d4da"),s=n.n(i);s.a},f5e3:function(t,e,n){},f986:function(t,e,n){"use strict";var i=n("3610"),s=n.n(i);s.a},fa25:function(t,e,n){"use strict";var i=n("c245"),s=n.n(i);s.a},fa44:function(t,e,n){"use strict";var i=n("622c"),s=n.n(i);s.a},fbb8:function(t,e,n){"use strict";var i=n("f09b"),s=n.n(i);s.a},fff9: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..271a1f1 --- /dev/null +++ b/kirby/panel/dist/js/plugins.js @@ -0,0 +1,49 @@ + +window.panel = window.panel || {}; +window.panel.plugins = { + components: {}, + fields: {}, + 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; + }); + + // 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); + }); + + // Views + resolve(parts, "views", function (name, options) { + window.panel.plugins["views"][name] = options; + }); +}; + +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..5bbc9a7 --- /dev/null +++ b/kirby/panel/dist/js/vendor.js @@ -0,0 +1,39 @@ +(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-vendors"],{"01f9":function(t,e,n){"use strict";var r=n("2d00"),i=n("5ca1"),o=n("2aba"),a=n("32e9"),s=n("84f2"),c=n("41a0"),u=n("7f20"),l=n("38fd"),f=n("2b4c")("iterator"),p=!([].keys&&"next"in[].keys()),d="@@iterator",h="keys",v="values",m=function(){return this};t.exports=function(t,e,n,y,g,b,_){c(n,e,y);var w,x,$,O=function(t){if(!p&&t in A)return A[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)}},C=e+" Iterator",S=g==v,k=!1,A=t.prototype,E=A[f]||A[d]||g&&A[g],M=E||O(g),T=g?S?O("entries"):M:void 0,j="Array"==e&&A.entries||E;if(j&&($=l(j.call(new t)),$!==Object.prototype&&$.next&&(u($,C,!0),r||"function"==typeof $[f]||a($,f,m))),S&&E&&E.name!==v&&(k=!0,M=function(){return E.call(this)}),r&&!_||!p&&!k&&A[f]||a(A,f,M),s[e]=M,s[C]=m,g)if(w={values:S?M:O(v),keys:b?M:O(h),entries:T},_)for(x in w)x in A||o(A,x,w[x]);else i(i.P+i.F*(p||k),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:(o=s.charCodeAt(c),o<55296||o>56319||c+1===u||(a=s.charCodeAt(c+1))<56320||a>57343?t?s.charAt(c):o:t?s.slice(c,c+2):a-56320+(o-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)}},"097d":function(t,e,n){"use strict";var r=n("5ca1"),i=n("8378"),o=n("7726"),a=n("ebd6"),s=n("bcaa");r(r.P+r.R,"Promise",{finally:function(t){var e=a(this,i.Promise||o.Promise),n="function"==typeof t;return this.then(n?function(n){return s(e,t()).then(function(){return n})}:t,n?function(n){return s(e,t()).then(function(){throw n})}:t)}})},"0a49":function(t,e,n){var r=n("9b43"),i=n("626a"),o=n("4bf8"),a=n("9def"),s=n("cd1c");t.exports=function(t,e){var n=1==t,c=2==t,u=3==t,l=4==t,f=6==t,p=5==t||f,d=e||s;return function(e,s,h){for(var v,m,y=o(e),g=i(y),b=r(s,h,3),_=a(g.length),w=0,x=n?d(e,_):c?d(e,0):void 0;_>w;w++)if((p||w in g)&&(v=g[w],m=b(v,w,y),t))if(n)x[w]=m;else if(m)switch(t){case 3:return!0;case 5:return v;case 6:return w;case 2:x.push(v)}else if(l)return!1;return f?-1:u||l?l:x}}},"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"),i=n("e11e");t.exports=Object.keys||function(t){return r(t,i)}},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"),i=n("4630"),o=n("6821"),a=n("6a99"),s=n("69a8"),c=n("c69a"),u=Object.getOwnPropertyDescriptor;e.f=n("9e1e")?u:function(t,e){if(t=o(t),e=a(e,!0),c)try{return u(t,e)}catch(n){}if(s(t,e))return i(!r.f.call(t,e),t[e])}},1495:function(t,e,n){var r=n("86cc"),i=n("cb7c"),o=n("0d58");t.exports=n("9e1e")?Object.defineProperties:function(t,e){i(t);var n,a=o(e),s=a.length,c=0;while(s>c)r.f(t,n=a[c++],e[n]);return t}},1516:function(t,e,n){"use strict";"function"===typeof Symbol&&Symbol.iterator;var r=Object.assign||function(t){for(var e=1;en-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),d=t.item},onDragAdd:function(t){this.updateEvenemt(t);var e=t.item._underlying_vm_;if(void 0!==e){n(t.item);var r=this.getVmIndex(t.newIndex);this.spliceList(r,0,e),this.computeIndexes();var i={element:e,newIndex:r};this.emitChanges({added:i})}},onDragRemove:function(t){if(this.updateEvenemt(t),o(this.rootContainer,t.item,t.oldIndex),this.isCloning)n(t.clone);else{var e=this.context.index;this.spliceList(e,1);var r={element:this.context.element,oldIndex:e};this.resetTransitionData(e),this.emitChanges({removed:r})}},onDragUpdate:function(t){this.updateEvenemt(t),n(t.item),o(t.from,t.item,t.oldIndex);var e=this.context.index,r=this.getVmIndex(t.newIndex);this.updatePosition(e,r);var i={element:this.context.element,oldIndex:e,newIndex:r};this.emitChanges({moved:i})},updateEvenemt:function(t){this.updateProperty(t,"newIndex"),this.updateProperty(t,"oldIndex")},updateProperty:function(t,e){t.hasOwnProperty(e)&&(t[e]+=this.headerOffset)},computeFutureIndex:function(t,e){if(!t.element)return 0;var n=[].concat(i(e.to.children)).filter(function(t){return"none"!==t.style["display"]}),r=n.indexOf(e.related),o=t.component.getVmIndex(r),a=-1!=n.indexOf(d);return a||!e.willInsertAfter?o:o+1},onDragMove:function(t,e){var n=this.move;if(!n||!this.realList)return!0;var i=this.getRelatedContextFromMoveEvent(t),o=this.context,a=this.computeFutureIndex(i,t);return r(o,{futureIndex:a}),r(t,{relatedContext:i,draggedContext:o}),n(t,e)},onDragEnd:function(t){this.computeIndexes(),d=null}}};return v}Array.from||(Array.from=function(t){return[].slice.call(t)});var a=n("53fe");t.exports=o(a)})()},1991:function(t,e,n){var r,i,o,a=n("9b43"),s=n("31f4"),c=n("fab2"),u=n("230e"),l=n("7726"),f=l.process,p=l.setImmediate,d=l.clearImmediate,h=l.MessageChannel,v=l.Dispatch,m=0,y={},g="onreadystatechange",b=function(){var t=+this;if(y.hasOwnProperty(t)){var e=y[t];delete y[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 y[++m]=function(){s("function"==typeof t?t:Function(t),e)},r(m),m},d=function(t){delete y[t]},"process"==n("2d95")(f)?r=function(t){f.nextTick(a(b,t,1))}:v&&v.now?r=function(t){v.now(a(b,t,1))}:h?(i=new h,o=i.port2,i.port1.onmessage=_,r=a(o.postMessage,o,1)):l.addEventListener&&"function"==typeof postMessage&&!l.importScripts?(r=function(t){l.postMessage(t+"","*")},l.addEventListener("message",_,!1)):r=g in u("script")?function(t){c.appendChild(u("script"))[g]=function(){c.removeChild(this),b.call(t)}}:function(t){setTimeout(a(b,t,1),0)}),t.exports={set:p,clear:d}},"19e9":function(t,e,n){var r,i,o; +/*! + 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){i=[t,e],r=a,o="function"===typeof r?r.apply(e,i):r,void 0===o||(t.exports=o)})(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(c){r=function(t){var e=document.createEvent("Event");return e.initEvent(t,!0,!1),e}}function i(t){if(t&&t.nodeName&&"TEXTAREA"===t.nodeName&&!n.has(t)){var e=null,i=null,o=null,a=function(){t.clientWidth!==i&&p()},s=function(e){window.removeEventListener("resize",a,!1),t.removeEventListener("input",p,!1),t.removeEventListener("keyup",p,!1),t.removeEventListener("autosize:destroy",s,!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",s,!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:s,update:p}),c()}function c(){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 l(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 f(){if(0!==t.scrollHeight){var n=l(t),r=document.documentElement&&document.documentElement.scrollTop;t.style.height="",t.style.height=t.scrollHeight+e+"px",i=t.clientWidth,n.forEach(function(t){t.node.scrollTop=t.scrollTop}),r&&(document.documentElement.scrollTop=r)}}function p(){f();var e=Math.round(parseFloat(t.style.height)),n=window.getComputedStyle(t,null),i="content-box"===n.boxSizing?Math.round(parseFloat(n.height)):t.offsetHeight;if(i1?a:a.$sub[0]:null;return{output:i,params:s}}},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}}}),c=e.extend({data:function(){return{dirty:!1,validations:null,lazyModel:null,model:null,prop:null,lazyParentModel:null,rootModel:null}},methods:r({},m,{refProxy:function(t){return this.getRef(t).proxy},getRef:function(t){return this.refs[t]},isNested:function(t){return"function"!==typeof this.validations[t]}}),computed: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=s(this.keys,function(e){return{enumerable:!0,configurable:!0,get:function(){return t.refProxy(e)}}}),n=s(y,function(e){return{enumerable:!0,configurable:!0,get:function(){return t[e]}}}),i=s(g,function(e){return{enumerable:!1,configurable:!0,get:function(){return t[e]}}});return Object.defineProperties({},r({},e,n,i))},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=c.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}}}}}),_=c.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""+f(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(),o=r({},e);delete o["$trackBy"];var a={};return this.keys.map(function(e){var r=t.tracker(e);return a.hasOwnProperty(r)?null:(a[r]=!0,(0,i.h)(c,r,{validations:o,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,i.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,o=s(n,function(t){return function(){return f(r,r.$v,t)}},function(t){return Array.isArray(t)?t.join("."):t});return(0,i.h)(v,e,{validations:o,lazyParentModel:a,prop:e,lazyModel:a,rootModel:r})}return(0,i.h)(c,e,{validations:n,lazyParentModel:t.getModel,prop:e,lazyModel:t.getModelKey,rootModel:t.rootModel})},x=function(t,e){return(0,i.h)(n,e,{rule:t.validations[e],lazyParentModel:t.lazyParentModel,lazyModel:t.getModel,rootModel:t.rootModel})};return b={VBase:e,Validation:c},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 $=function(t,e){var n=x(t),r=_(n),o=r.Validation,s=r.VBase,c=new s({computed:{children:function(){var n="function"===typeof e?e.call(t):e;return[(0,i.h)(o,"$v",{validations:n,lazyParentModel:a,prop:"$v",model:t,rootModel:t})]}}});return c},O={data:function(){var t=this.$options.validations;return t&&(this._vuelidate=$(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 C(t){t.mixin(O)}e.Vuelidate=C,e.validationMixin=O,e.withParams=o.withParams,e.default=C},"1fa8":function(t,e,n){var r=n("cb7c");t.exports=function(t,e,n,i){try{return i?e(r(n)[0],n[1]):e(n)}catch(a){var o=t["return"];throw void 0!==o&&r(o.call(t)),a}}},"20d6":function(t,e,n){"use strict";var r=n("5ca1"),i=n("0a49")(6),o="findIndex",a=!0;o in[]&&Array(1)[o](function(){a=!1}),r(r.P+r.F*a,"Array",{findIndex:function(t){return i(this,t,arguments.length>1?arguments[1]:void 0)}}),n("9c6c")(o)},"214f":function(t,e,n){"use strict";n("b0c5");var r=n("2aba"),i=n("32e9"),o=n("79e5"),a=n("be13"),s=n("2b4c"),c=n("520a"),u=s("species"),l=!o(function(){var t=/./;return t.exec=function(){var t=[];return t.groups={a:"7"},t},"7"!=="".replace(t,"$")}),f=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=s(t),d=!o(function(){var e={};return e[p]=function(){return 7},7!=""[t](e)}),h=d?!o(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&&!l||"split"===t&&!f){var v=/./[p],m=n(a,p,""[t],function(t,e,n,r,i){return e.exec===c?d&&!i?{done:!0,value:v.call(e,n,r)}:{done:!0,value:t.call(n,e,r)}:{done:!1}}),y=m[0],g=m[1];r(String.prototype,t,y),i(RegExp.prototype,p,2==e?function(t,e){return g.call(t,this,e)}:function(t){return g.call(t,this)})}}},"230e":function(t,e,n){var r=n("d3f4"),i=n("7726").document,o=r(i)&&r(i.createElement);t.exports=function(t){return o?i.createElement(t):{}}},"23c6":function(t,e,n){var r=n("2d95"),i=n("2b4c")("toStringTag"),o="Arguments"==r(function(){return arguments}()),a=function(t,e){try{return t[e]}catch(n){}};t.exports=function(t){var e,n,s;return void 0===t?"Undefined":null===t?"Null":"string"==typeof(n=a(e=Object(t),i))?n:o?r(e):"Object"==(s=r(e))&&"function"==typeof e.callee?"Arguments":s}},2621:function(t,e){e.f=Object.getOwnPropertySymbols},"27ee":function(t,e,n){var r=n("23c6"),i=n("2b4c")("iterator"),o=n("84f2");t.exports=n("8378").getIteratorMethod=function(t){if(void 0!=t)return t[i]||t["@@iterator"]||o[r(t)]}},2877:function(t,e,n){"use strict";function r(t,e,n,r,i,o,a,s){var c,u="function"===typeof t?t.options:t;if(e&&(u.render=e,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),o&&(u._scopeId="data-v-"+o),a?(c=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__),i&&i.call(this,t),t&&t._registeredComponents&&t._registeredComponents.add(a)},u._ssrRegister=c):i&&(c=s?function(){i.call(this,this.$root.$options.shadowRoot)}:i),c)if(u.functional){u._injectStyles=c;var l=u.render;u.render=function(t,e){return c.call(e),l(t,e)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,c):[c]}return{exports:t,options:u}}n.d(e,"a",function(){return r})},"28a5":function(t,e,n){"use strict";var r=n("aae3"),i=n("cb7c"),o=n("ebd6"),a=n("0390"),s=n("9def"),c=n("5f1b"),u=n("520a"),l=Math.min,f=[].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,m){var y=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]?y=function(t,e){var i=String(this);if(void 0===t&&0===e)return[];if(!r(t))return n.call(i,t,e);var o,a,s,c=[],l=(t.ignoreCase?"i":"")+(t.multiline?"m":"")+(t.unicode?"u":"")+(t.sticky?"y":""),p=0,v=void 0===e?4294967295:e>>>0,m=new RegExp(t.source,l+"g");while(o=u.call(m,i)){if(a=m[h],a>p&&(c.push(i.slice(p,o.index)),o[d]>1&&o.index=v))break;m[h]===o.index&&m[h]++}return p===i[d]?!s&&m.test("")||c.push(""):c.push(i.slice(p)),c[d]>v?c.slice(0,v):c}:"0"[p](void 0,0)[d]&&(y=function(t,e){return void 0===t&&0===e?[]:n.call(this,t,e)}),[function(n,r){var i=t(this),o=void 0==n?void 0:n[e];return void 0!==o?o.call(n,i,r):y.call(String(i),n,r)},function(t,e){var r=m(y,t,this,e,y!==n);if(r.done)return r.value;var u=i(t),f=String(this),p=o(u,RegExp),d=u.unicode,h=(u.ignoreCase?"i":"")+(u.multiline?"m":"")+(u.unicode?"u":"")+(v?"y":"g"),g=new p(v?u:"^(?:"+u.source+")",h),b=void 0===e?4294967295:e>>>0;if(0===b)return[];if(0===f.length)return null===c(g,f)?[f]:[];var _=0,w=0,x=[];while(w";e.style.display="none",n("fab2").appendChild(e),e.src="javascript:",t=e.contentWindow.document,t.open(),t.write(i+"script"+a+"document.F=Object"+i+"/script"+a),t.close(),u=t.F;while(r--)delete u[c][o[r]];return u()};t.exports=Object.create||function(t,e){var n;return null!==t?(s[c]=r(t),n=new s,s[c]=null,n[a]=t):n=u(),void 0===e?n:i(n,e)}},"2b4c":function(t,e,n){var r=n("5537")("wks"),i=n("ca5a"),o=n("7726").Symbol,a="function"==typeof o,s=t.exports=function(t){return r[t]||(r[t]=a&&o[t]||(a?o:i)("Symbol."+t))};s.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)}},"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.0.1 + * (c) 2017 Evan You + * @license MIT + */var r=function(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)}},i="undefined"!==typeof window&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__;function o(t){i&&(t._devtoolHook=i,i.emit("vuex:init",t),i.on("vuex:travel-to-state",function(e){t.replaceState(e)}),t.subscribe(function(t,e){i.emit("vuex:mutation",t,e)}))}function a(t,e){Object.keys(t).forEach(function(n){return e(t[n],n)})}function s(t){return null!==t&&"object"===typeof t}function c(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)||{}},l={namespaced:{configurable:!0}};l.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,l);var f=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])}}f.prototype.get=function(t){return t.reduce(function(t,e){return t.getChild(e)},this.root)},f.prototype.getNamespace=function(t){var e=this.root;return t.reduce(function(t,n){return e=e.getChild(n),t+(e.namespaced?n+"/":"")},"")},f.prototype.update=function(t){p([],this.root,t)},f.prototype.register=function(t,e,n){var r=this;void 0===n&&(n=!0);var i=new u(e,n);if(0===t.length)this.root=i;else{var o=this.get(t.slice(0,-1));o.addChild(t[t.length-1],i)}e.modules&&a(e.modules,function(e,i){r.register(t.concat(i),e,n)})},f.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&&A(window.Vue);var n=t.plugins;void 0===n&&(n=[]);var r=t.strict;void 0===r&&(r=!1);var i=t.state;void 0===i&&(i={}),"function"===typeof i&&(i=i()||{}),this._committing=!1,this._actions=Object.create(null),this._actionSubscribers=[],this._mutations=Object.create(null),this._wrappedGetters=Object.create(null),this._modules=new f(t),this._modulesNamespaceMap=Object.create(null),this._subscribers=[],this._watcherVM=new d;var a=this,s=this,c=s.dispatch,u=s.commit;this.dispatch=function(t,e){return c.call(a,t,e)},this.commit=function(t,e,n){return u.call(a,t,e,n)},this.strict=r,b(this,i,[],this._modules.root),g(this,i),n.forEach(function(t){return t(e)}),d.config.devtools&&o(this)},v={state:{configurable:!0}};function m(t,e){return e.indexOf(t)<0&&e.push(t),function(){var n=e.indexOf(t);n>-1&&e.splice(n,1)}}function y(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),g(t,n,e)}function g(t,e,n){var r=t._vm;t.getters={};var i=t._wrappedGetters,o={};a(i,function(e,n){o[n]=function(){return e(t)},Object.defineProperty(t.getters,n,{get:function(){return t._vm[n]},enumerable:!0})});var s=d.config.silent;d.config.silent=!0,t._vm=new d({data:{$$state:e},computed:o}),d.config.silent=s,t.strict&&C(t),r&&(n&&t._withCommit(function(){r._data.$$state=null}),d.nextTick(function(){return r.$destroy()}))}function b(t,e,n,r,i){var o=!n.length,a=t._modules.getNamespace(n);if(r.namespaced&&(t._modulesNamespaceMap[a]=r),!o&&!i){var s=S(e,n.slice(0,-1)),c=n[n.length-1];t._withCommit(function(){d.set(s,c,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,i=e.handler||e;$(t,r,i,u)}),r.forEachGetter(function(e,n){var r=a+n;O(t,r,e,u)}),r.forEachChild(function(r,o){b(t,e,n.concat(o),r,i)})}function _(t,e,n){var r=""===e,i={dispatch:r?t.dispatch:function(n,r,i){var o=k(n,r,i),a=o.payload,s=o.options,c=o.type;return s&&s.root||(c=e+c),t.dispatch(c,a)},commit:r?t.commit:function(n,r,i){var o=k(n,r,i),a=o.payload,s=o.options,c=o.type;s&&s.root||(c=e+c),t.commit(c,a,s)}};return Object.defineProperties(i,{getters:{get:r?function(){return t.getters}:function(){return w(t,e)}},state:{get:function(){return S(t.state,n)}}}),i}function w(t,e){var n={},r=e.length;return Object.keys(t.getters).forEach(function(i){if(i.slice(0,r)===e){var o=i.slice(r);Object.defineProperty(n,o,{get:function(){return t.getters[i]},enumerable:!0})}}),n}function x(t,e,n,r){var i=t._mutations[e]||(t._mutations[e]=[]);i.push(function(e){n.call(t,r.state,e)})}function $(t,e,n,r){var i=t._actions[e]||(t._actions[e]=[]);i.push(function(e,i){var o=n.call(t,{dispatch:r.dispatch,commit:r.commit,getters:r.getters,state:r.state,rootGetters:t.getters,rootState:t.state},e,i);return c(o)||(o=Promise.resolve(o)),t._devtoolHook?o.catch(function(e){throw t._devtoolHook.emit("vuex:error",e),e}):o})}function O(t,e,n,r){t._wrappedGetters[e]||(t._wrappedGetters[e]=function(t){return n(r.state,r.getters,t.state,t.getters)})}function C(t){t._vm.$watch(function(){return this._data.$$state},function(){0},{deep:!0,sync:!0})}function S(t,e){return e.length?e.reduce(function(t,e){return t[e]},t):t}function k(t,e,n){return s(t)&&t.type&&(n=e,e=t,t=t.type),{type:t,payload:e,options:n}}function A(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,i=k(t,e,n),o=i.type,a=i.payload,s=(i.options,{type:o,payload:a}),c=this._mutations[o];c&&(this._withCommit(function(){c.forEach(function(t){t(a)})}),this._subscribers.forEach(function(t){return t(s,r.state)}))},h.prototype.dispatch=function(t,e){var n=this,r=k(t,e),i=r.type,o=r.payload,a={type:i,payload:o},s=this._actions[i];if(s)return this._actionSubscribers.forEach(function(t){return t(a,n.state)}),s.length>1?Promise.all(s.map(function(t){return t(o)})):s[0](o)},h.prototype.subscribe=function(t){return m(t,this._subscribers)},h.prototype.subscribeAction=function(t){return m(t,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),g(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=S(e.state,t.slice(0,-1));d.delete(n,t[t.length-1])}),y(this)},h.prototype.hotUpdate=function(t){this._modules.update(t),y(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,i=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 i?i.call(this,e,n):e[i]},n[r].vuex=!0}),n}),M=L(function(t,e){var n={};return D(e).forEach(function(e){var r=e.key,i=e.val;n[r]=function(){var e=[],n=arguments.length;while(n--)e[n]=arguments[n];var r=this.$store.commit;if(t){var o=I(this.$store,"mapMutations",t);if(!o)return;r=o.context.commit}return"function"===typeof i?i.apply(this,[r].concat(e)):r.apply(this.$store,[i].concat(e))}}),n}),T=L(function(t,e){var n={};return D(e).forEach(function(e){var r=e.key,i=e.val;i=t+i,n[r]=function(){if(!t||I(this.$store,"mapGetters",t))return this.$store.getters[i]},n[r].vuex=!0}),n}),j=L(function(t,e){var n={};return D(e).forEach(function(e){var r=e.key,i=e.val;n[r]=function(){var e=[],n=arguments.length;while(n--)e[n]=arguments[n];var r=this.$store.dispatch;if(t){var o=I(this.$store,"mapActions",t);if(!o)return;r=o.context.dispatch}return"function"===typeof i?i.apply(this,[r].concat(e)):r.apply(this.$store,[i].concat(e))}}),n}),P=function(t){return{mapState:E.bind(null,t),mapGetters:T.bind(null,t),mapMutations:M.bind(null,t),mapActions:j.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:A,version:"3.0.1",mapState:E,mapMutations:M,mapGetters:T,mapActions:j,createNamespacedHelpers:P};e["a"]=N},"2fdb":function(t,e,n){"use strict";var r=n("5ca1"),i=n("d2c8"),o="includes";r(r.P+r.F*n("5147")(o),"String",{includes:function(t){return!!~i(this,t,o).indexOf(t,arguments.length>1?arguments[1]:void 0)}})},"31f4":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)}},"32e9":function(t,e,n){var r=n("86cc"),i=n("4630");t.exports=n("9e1e")?function(t,e,n){return r.f(t,e,i(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)})}},"33a4":function(t,e,n){var r=n("84f2"),i=n("2b4c")("iterator"),o=Array.prototype;t.exports=function(t){return void 0!==t&&(r.Array===t||o[i]===t)}},3835:function(t,e,n){"use strict";function r(t){if(Array.isArray(t))return t}function i(t,e){var n=[],r=!0,i=!1,o=void 0;try{for(var a,s=t[Symbol.iterator]();!(r=(a=s.next()).done);r=!0)if(n.push(a.value),e&&n.length===e)break}catch(c){i=!0,o=c}finally{try{r||null==s["return"]||s["return"]()}finally{if(i)throw o}}return n}function o(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}function a(t,e){return r(t)||i(t,e)||o()}n.d(e,"a",function(){return a})},3846:function(t,e,n){n("9e1e")&&"g"!=/./g.flags&&n("86cc").f(RegExp.prototype,"flags",{configurable:!0,get:n("0bfb")})},"386b":function(t,e,n){var r=n("5ca1"),i=n("79e5"),o=n("be13"),a=/"/g,s=function(t,e,n,r){var i=String(o(t)),s="<"+e;return""!==n&&(s+=" "+n+'="'+String(r).replace(a,""")+'"'),s+">"+i+""};t.exports=function(t,e){var n={};n[t]=e(s),r(r.P+r.F*i(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"),i=n("83a1"),o=n("5f1b");n("214f")("search",1,function(t,e,n,a){return[function(n){var r=t(this),i=void 0==n?void 0:n[e];return void 0!==i?i.call(n,r):new RegExp(n)[e](String(r))},function(t){var e=a(n,t,this);if(e.done)return e.value;var s=r(t),c=String(this),u=s.lastIndex;i(u,0)||(s.lastIndex=0);var l=o(s,c);return i(s.lastIndex,u)||(s.lastIndex=u),null===l?-1:l.index}]})},"38fd":function(t,e,n){var r=n("69a8"),i=n("4bf8"),o=n("613b")("IE_PROTO"),a=Object.prototype;t.exports=Object.getPrototypeOf||function(t){return t=i(t),r(t,o)?t[o]:"function"==typeof t.constructor&&t instanceof t.constructor?t.constructor.prototype:t instanceof Object?a:null}},"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]*$/)},"3b2b":function(t,e,n){var r=n("7726"),i=n("5dbc"),o=n("86cc").f,a=n("9093").f,s=n("aae3"),c=n("0bfb"),u=r.RegExp,l=u,f=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=s(t),o=void 0===e;return!n&&r&&t.constructor===u&&o?t:i(h?new l(r&&!o?t.source:t,e):l((r=t instanceof u)?t.source:t,r&&o?c.call(t):e),n?this:f,u)};for(var v=function(t){t in u||o(u,t,{configurable:!0,get:function(){return l[t]},set:function(e){l[t]=e}})},m=a(l),y=0;m.length>y;)v(m[y++]);f.constructor=u,u.prototype=f,n("2aba")(r,"RegExp",u)}n("7a56")("RegExp")},"41a0":function(t,e,n){"use strict";var r=n("2aeb"),i=n("4630"),o=n("7f20"),a={};n("32e9")(a,n("2b4c")("iterator"),function(){return this}),t.exports=function(t,e,n){t.prototype=r(a,{next:i(1,n)}),o(t,e+" Iterator")}},"456d":function(t,e,n){var r=n("4bf8"),i=n("0d58");n("5eda")("keys",function(){return function(t){return i(r(t))}})},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]*$/)},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})}},4917:function(t,e,n){"use strict";var r=n("cb7c"),i=n("9def"),o=n("0390"),a=n("5f1b");n("214f")("match",1,function(t,e,n,s){return[function(n){var r=t(this),i=void 0==n?void 0:n[e];return void 0!==i?i.call(n,r):new RegExp(n)[e](String(r))},function(t){var e=s(n,t,this);if(e.done)return e.value;var c=r(t),u=String(this);if(!c.global)return a(c,u);var l=c.unicode;c.lastIndex=0;var f,p=[],d=0;while(null!==(f=a(c,u))){var h=String(f[0]);p[d]=h,""===h&&(c.lastIndex=o(u,i(c.lastIndex),l)),d++}return 0===d?null:p}]})},"4a59":function(t,e,n){var r=n("9b43"),i=n("1fa8"),o=n("33a4"),a=n("cb7c"),s=n("9def"),c=n("27ee"),u={},l={};e=t.exports=function(t,e,n,f,p){var d,h,v,m,y=p?function(){return t}:c(t),g=r(n,f,e?2:1),b=0;if("function"!=typeof y)throw TypeError(t+" is not iterable!");if(o(y)){for(d=s(t.length);d>b;b++)if(m=e?g(a(h=t[b])[0],h[1]):g(t[b]),m===u||m===l)return m}else for(v=y.call(t);!(h=v.next()).done;)if(m=i(v,g,h.value,e),m===u||m===l)return m};e.BREAK=u,e.RETURN=l},"4bf8":function(t,e,n){var r=n("be13");t.exports=function(t){return Object(r(t))}},"504c":function(t,e,n){var r=n("0d58"),i=n("6821"),o=n("52a7").f;t.exports=function(t){return function(e){var n,a=i(e),s=r(a),c=s.length,u=0,l=[];while(c>u)o.call(a,n=s[u++])&&l.push(t?[n,a[n]]:a[n]);return l}}},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(i){}}return!0}},"520a":function(t,e,n){"use strict";var r=n("0bfb"),i=RegExp.prototype.exec,o=String.prototype.replace,a=i,s="lastIndex",c=function(){var t=/a/,e=/b*/g;return i.call(t,"a"),i.call(e,"a"),0!==t[s]||0!==e[s]}(),u=void 0!==/()??/.exec("")[1],l=c||u;l&&(a=function(t){var e,n,a,l,f=this;return u&&(n=new RegExp("^"+f.source+"$(?!\\s)",r.call(f))),c&&(e=f[s]),a=i.call(f,t),c&&a&&(f[s]=f.global?a.index+a[0].length:e),u&&a&&a.length>1&&o.call(a[0],n,function(){for(l=1;l + * @license MIT + */ +/**! + * Sortable + * @author RubaXa + * @license MIT + */ +(function(o){"use strict";r=o,i="function"===typeof r?r.call(e,n,e,t):r,void 0===i||(t.exports=i)})(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,i,o,a,s,c,u,l,f,p,d,h,v,m,y,g,b,_={},w=/\s+/g,x=/left|right|inline/,$="Sortable"+(new Date).getTime(),O=window,C=O.document,S=O.parseInt,k=O.setTimeout,A=O.jQuery||O.Zepto,E=O.Polymer,M=!1,T=!1,j="draggable"in C.createElement("div"),P=function(t){return!navigator.userAgent.match(/(?:Trident.*rv[ :]?11\.|msie)/i)&&(t=C.createElement("x"),t.style.cssText="pointer-events:auto","auto"===t.style.pointerEvents)}(),D=!1,L=Math.abs,I=Math.min,N=[],R=[],F=ot(function(t,e,n){if(n&&e.scroll){var r,i,o,a,l,f,p=n[$],d=e.scrollSensitivity,h=e.scrollSpeed,v=t.clientX,m=t.clientY,y=window.innerWidth,g=window.innerHeight;if(c!==n&&(s=e.scroll,c=n,u=e.scrollFn,!0===s)){s=n;do{if(s.offsetWidth-1:i==t)}}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};try{window.addEventListener("test",null,Object.defineProperty({},"passive",{get:function(){T=!1,M={capture:!1,passive:T}}}))}catch(ft){}function z(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be HTMLElement, and not "+{}.toString.call(t);this.el=t,this.options=e=at({},e),t[$]=this;var n={group:Math.random(),sort:!0,disabled:!1,store:null,handle:null,scroll:!0,scrollSensitivity:30,scrollSpeed:10,draggable:/[uo]l/i.test(t.nodeName)?"li":">*",ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==z.supportPointer};for(var r in n)!(r in e)&&(e[r]=n[r]);for(var i in U(e),this)"_"===i.charAt(0)&&"function"===typeof this[i]&&(this[i]=this[i].bind(this));this.nativeDraggable=!e.forceFallback&&j,Y(t,"mousedown",this._onTapStart),Y(t,"touchstart",this._onTapStart),e.supportPointer&&Y(t,"pointerdown",this._onTapStart),this.nativeDraggable&&(Y(t,"dragover",this),Y(t,"dragenter",this)),R.push(this._onDragOver),e.store&&this.sort(e.store.get(this))}function H(e,n){"clone"!==e.lastPullMode&&(n=!0),r&&r.state!==n&&(G(r,"display",n?"none":""),n||r.state&&(e.options.group.revertClone?(i.insertBefore(r,o),e._animate(t,r)):i.insertBefore(r,t)),r.state=n)}function B(t,e,n){if(t){n=n||C;do{if(">*"===e&&t.parentNode===n||it(t,e))return t}while(t=V(t))}return null}function V(t){var e=t.host;return e&&e.nodeType?e:t.parentNode}function q(t){t.dataTransfer&&(t.dataTransfer.dropEffect="move"),t.preventDefault()}function Y(t,e,n){t.addEventListener(e,n,M)}function K(t,e,n){t.removeEventListener(e,n,M)}function W(t,e,n){if(t)if(t.classList)t.classList[n?"add":"remove"](e);else{var r=(" "+t.className+" ").replace(w," ").replace(" "+e+" "," ");t.className=(r+(n?" "+e:"")).replace(w," ")}}function G(t,e,n){var r=t&&t.style;if(r){if(void 0===n)return C.defaultView&&C.defaultView.getComputedStyle?n=C.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];e in r||(e="-webkit-"+e),r[e]=n+("string"===typeof n?"":"px")}}function X(t,e,n){if(t){var r=t.getElementsByTagName(e),i=0,o=r.length;if(n)for(;i5||e.clientX-(r.left+r.width)>5}function nt(t){var e=t.tagName+t.className+t.src+t.href+t.textContent,n=e.length,r=0;while(n--)r+=e.charCodeAt(n);return r.toString(36)}function rt(t,e){var n=0;if(!t||!t.parentNode)return-1;while(t&&(t=t.previousElementSibling))"TEMPLATE"===t.nodeName.toUpperCase()||">*"!==e&&!it(t,e)||n++;return n}function it(t,e){if(t){e=e.split(".");var n=e.shift().toUpperCase(),r=new RegExp("\\s("+e.join("|")+")(?=\\s)","g");return(""===n||t.nodeName.toUpperCase()==n)&&(!e.length||((" "+t.className+" ").match(r)||[]).length==e.length)}return!1}function ot(t,e){var n,r;return function(){void 0===n&&(n=arguments,r=this,k(function(){1===n.length?t.call(r,n[0]):t.apply(r,n),n=void 0},e))}}function at(t,e){if(t&&e)for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}function st(t){return E&&E.dom?E.dom(t).cloneNode(!0):A?A(t).clone(!0)[0]:t.cloneNode(!0)}function ct(t){var e=t.getElementsByTagName("input"),n=e.length;while(n--){var r=e[n];r.checked&&N.push(r)}}function ut(t){return k(t,0)}function lt(t){return clearTimeout(t)}return z.prototype={constructor:z,_onTapStart:function(e){var n,r=this,i=this.el,o=this.options,s=o.preventOnFilter,c=e.type,u=e.touches&&e.touches[0],l=(u||e).target,f=e.target.shadowRoot&&e.path&&e.path[0]||l,p=o.filter;if(ct(i),!t&&!(/mousedown|pointerdown/.test(c)&&0!==e.button||o.disabled)&&!f.isContentEditable&&(l=B(l,o.draggable,i),l&&a!==l)){if(n=rt(l,o.draggable),"function"===typeof p){if(p.call(this,e,l,this))return J(r,f,"filter",l,i,i,n),void(s&&e.preventDefault())}else if(p&&(p=p.split(",").some(function(t){if(t=B(f,t.trim(),i),t)return J(r,t,"filter",l,i,i,n),!0}),p))return void(s&&e.preventDefault());o.handle&&!B(f,o.handle,i)||this._prepareDragStart(e,u,l,n)}},_prepareDragStart:function(n,r,s,c){var u,l=this,f=l.el,p=l.options,h=f.ownerDocument;s&&!t&&s.parentNode===f&&(y=n,i=f,t=s,e=t.parentNode,o=t.nextSibling,a=s,v=p.group,d=c,this._lastX=(r||n).clientX,this._lastY=(r||n).clientY,t.style["will-change"]="all",u=function(){l._disableDelayedDrag(),t.draggable=l.nativeDraggable,W(t,p.chosenClass,!0),l._triggerDragStart(n,r),J(l,i,"choose",t,i,i,d)},p.ignore.split(",").forEach(function(e){X(t,e.trim(),Q)}),Y(h,"mouseup",l._onDrop),Y(h,"touchend",l._onDrop),Y(h,"touchcancel",l._onDrop),Y(h,"selectstart",l),p.supportPointer&&Y(h,"pointercancel",l._onDrop),p.delay?(Y(h,"mouseup",l._disableDelayedDrag),Y(h,"touchend",l._disableDelayedDrag),Y(h,"touchcancel",l._disableDelayedDrag),Y(h,"mousemove",l._disableDelayedDrag),Y(h,"touchmove",l._disableDelayedDrag),p.supportPointer&&Y(h,"pointermove",l._disableDelayedDrag),l._dragStartTimer=k(u,p.delay)):u())},_disableDelayedDrag:function(){var t=this.el.ownerDocument;clearTimeout(this._dragStartTimer),K(t,"mouseup",this._disableDelayedDrag),K(t,"touchend",this._disableDelayedDrag),K(t,"touchcancel",this._disableDelayedDrag),K(t,"mousemove",this._disableDelayedDrag),K(t,"touchmove",this._disableDelayedDrag),K(t,"pointermove",this._disableDelayedDrag)},_triggerDragStart:function(e,n){n=n||("touch"==e.pointerType?e:null),n?(y={target:t,clientX:n.clientX,clientY:n.clientY},this._onDragStart(y,"touch")):this.nativeDraggable?(Y(t,"dragend",this),Y(i,"dragstart",this._onDragStart)):this._onDragStart(y,!0);try{C.selection?ut(function(){C.selection.empty()}):window.getSelection().removeAllRanges()}catch(ft){}},_dragStarted:function(){if(i&&t){var e=this.options;W(t,e.ghostClass,!0),W(t,e.dragClass,!1),z.active=this,J(this,i,"start",t,i,i,d)}else this._nulling()},_emulateDragOver:function(){if(g){if(this._lastX===g.clientX&&this._lastY===g.clientY)return;this._lastX=g.clientX,this._lastY=g.clientY,P||G(n,"display","none");var t=C.elementFromPoint(g.clientX,g.clientY),e=t,r=R.length;if(t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(g.clientX,g.clientY),e=t),e)do{if(e[$]){while(r--)R[r]({clientX:g.clientX,clientY:g.clientY,target:t,rootEl:e});break}t=e}while(e=e.parentNode);P||G(n,"display","")}},_onTouchMove:function(t){if(y){var e=this.options,r=e.fallbackTolerance,i=e.fallbackOffset,o=t.touches?t.touches[0]:t,a=o.clientX-y.clientX+i.x,s=o.clientY-y.clientY+i.y,c=t.touches?"translate3d("+a+"px,"+s+"px,0)":"translate("+a+"px,"+s+"px)";if(!z.active){if(r&&I(L(o.clientX-this._lastX),L(o.clientY-this._lastY))t.offsetWidth,T=s.offsetHeight>t.offsetHeight,j=(E?(a.clientX-u.left)/S:(a.clientY-u.top)/A)>.5,P=s.nextElementSibling,L=!1;if(E){var I=t.offsetTop,N=s.offsetTop;L=I===N?s.previousElementSibling===t&&!M||j&&M:s.previousElementSibling===t||t.previousElementSibling===s?(a.clientY-u.top)/A>.5:N>I}else O||(L=P!==t&&!T||j&&T);var R=Z(i,h,t,c,s,u,a,L);!1!==R&&(1!==R&&-1!==R||(L=1===R),D=!0,k(tt,30),H(_,w),t.contains(h)||(L&&!P?h.appendChild(t):s.parentNode.insertBefore(t,L?P:s)),e=t.parentNode,this._animate(c,t),this._animate(u,s))}}},_animate:function(t,e){var n=this.options.animation;if(n){var r=e.getBoundingClientRect();1===t.nodeType&&(t=t.getBoundingClientRect()),G(e,"transition","none"),G(e,"transform","translate3d("+(t.left-r.left)+"px,"+(t.top-r.top)+"px,0)"),e.offsetWidth,G(e,"transition","all "+n+"ms"),G(e,"transform","translate3d(0,0,0)"),clearTimeout(e.animated),e.animated=k(function(){G(e,"transition",""),G(e,"transform",""),e.animated=!1},n)}},_offUpEvents:function(){var t=this.el.ownerDocument;K(C,"touchmove",this._onTouchMove),K(C,"pointermove",this._onTouchMove),K(t,"mouseup",this._onDrop),K(t,"touchend",this._onDrop),K(t,"pointerup",this._onDrop),K(t,"touchcancel",this._onDrop),K(t,"pointercancel",this._onDrop),K(t,"selectstart",this)},_onDrop:function(a){var s=this.el,c=this.options;clearInterval(this._loopId),clearInterval(_.pid),clearTimeout(this._dragStartTimer),lt(this._cloneId),lt(this._dragStartId),K(C,"mouseover",this),K(C,"mousemove",this._onTouchMove),this.nativeDraggable&&(K(C,"drop",this),K(s,"dragstart",this._onDragStart)),this._offUpEvents(),a&&(b&&(a.preventDefault(),!c.dropBubble&&a.stopPropagation()),n&&n.parentNode&&n.parentNode.removeChild(n),i!==e&&"clone"===z.active.lastPullMode||r&&r.parentNode&&r.parentNode.removeChild(r),t&&(this.nativeDraggable&&K(t,"dragend",this),Q(t),t.style["will-change"]="",W(t,this.options.ghostClass,!1),W(t,this.options.chosenClass,!1),J(this,i,"unchoose",t,e,i,d),i!==e?(h=rt(t,c.draggable),h>=0&&(J(null,e,"add",t,e,i,d,h),J(this,i,"remove",t,e,i,d,h),J(null,e,"sort",t,e,i,d,h),J(this,i,"sort",t,e,i,d,h))):t.nextSibling!==o&&(h=rt(t,c.draggable),h>=0&&(J(this,i,"update",t,e,i,d,h),J(this,i,"sort",t,e,i,d,h))),z.active&&(null!=h&&-1!==h||(h=d),J(this,i,"end",t,e,i,d,h),this.save()))),this._nulling()},_nulling:function(){i=t=e=n=o=r=a=s=c=y=g=b=h=l=f=m=v=z.active=null,N.forEach(function(t){t.checked=!0}),N.length=0},handleEvent:function(e){switch(e.type){case"drop":case"dragend":this._onDrop(e);break;case"dragover":case"dragenter":t&&(this._onDragOver(e),q(e));break;case"mouseover":this._onDrop(e);break;case"selectstart":e.preventDefault();break}},toArray:function(){for(var t,e=[],n=this.el.children,r=0,i=n.length,o=this.options;ro)a(n[o++]);t._c=[],t._n=!1,e&&!t._h&&L(t)})}},L=function(t){y.call(c,function(){var e,n,r,i=t._v,o=I(t);if(o&&(e=_(function(){E?C.emit("unhandledRejection",i,t):(n=c.onunhandledrejection)?n({promise:t,reason:i}):(r=c.console)&&r.error&&r.error("Unhandled promise rejection",i)}),t._h=E||I(t)?2:1),t._a=void 0,o&&e.e)throw e.v})},I=function(t){return 1!==t._h&&0===(t._a||t._c).length},N=function(t){y.call(c,function(){var e;E?C.emit("rejectionHandled",t):(e=c.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 O("Promise can't be resolved itself");(e=P(t))?g(function(){var r={_w:n,_d:!1};try{e.call(t,u(F,r,1),u(R,r,1))}catch(i){R.call(r,i)}}):(n._v=t,n._s=1,D(n,!1))}catch(r){R.call({_w:n,_d:!1},r)}}};j||(A=function(t){h(this,A,$,"_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("dcbc")(A.prototype,{then:function(t,e){var n=T(m(this,A));return n.ok="function"!=typeof t||t,n.fail="function"==typeof e&&e,n.domain=E?C.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)}}),o=function(){var t=new r;this.promise=t,this.resolve=u(F,t,1),this.reject=u(R,t,1)},b.f=T=function(t){return t===A||t===a?new o(t):i(t)}),f(f.G+f.W+f.F*!j,{Promise:A}),n("7f20")(A,$),n("7a56")($),a=n("8378")[$],f(f.S+f.F*!j,$,{reject:function(t){var e=T(this),n=e.reject;return n(t),e.promise}}),f(f.S+f.F*(s||!j),$,{resolve:function(t){return x(s&&this===a?A:this,t)}}),f(f.S+f.F*!(j&&n("5cc5")(function(t){A.all(t)["catch"](M)})),$,{all:function(t){var e=this,n=T(e),r=n.resolve,i=n.reject,o=_(function(){var n=[],o=0,a=1;v(t,!1,function(t){var s=o++,c=!1;n.push(void 0),a++,e.resolve(t).then(function(t){c||(c=!0,n[s]=t,--a||r(n))},i)}),--a||r(n)});return o.e&&i(o.v),n.promise},race:function(t){var e=this,n=T(e),r=n.reject,i=_(function(){v(t,!1,function(t){e.resolve(t).then(n.resolve,r)})});return i.e&&r(i.v),n.promise}})},5537:function(t,e,n){var r=n("8378"),i=n("7726"),o="__core-js_shared__",a=i[o]||(i[o]={});(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:"© 2018 Denis Pushkarev (zloirock.ru)"})},"55dd":function(t,e,n){"use strict";var r=n("5ca1"),i=n("d8e8"),o=n("4bf8"),a=n("79e5"),s=[].sort,c=[1,2,3];r(r.P+r.F*(a(function(){c.sort(void 0)})||!a(function(){c.sort(null)})||!n("2f21")(s)),"Array",{sort:function(t){return void 0===t?s.call(o(this)):s.call(o(this),i(t))}})},"5a0c":function(t,e,n){!function(e,n){t.exports=n()}(0,function(){"use strict";var t="millisecond",e="second",n="minute",r="hour",i="day",o="week",a="month",s="year",c=/^(\d{4})-?(\d{1,2})-?(\d{0,2})(.*?(\d{1,2}):(\d{1,2}):(\d{1,2}))?.?(\d{1,3})?$/,u=/\[.*?\]|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={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("_")},f=function(t,e,n){var r=String(t);return!r||r.length>=e?t:""+Array(e+1-r.length).join(n)+t},p={padStart:f,padZoneStr:function(t){var e=Math.abs(t),n=Math.floor(e/60),r=e%60;return(t<=0?"+":"-")+f(n,2,"0")+":"+f(r,2,"0")},monthDiff:function(t,e){var n=12*(e.year()-t.year())+(e.month()-t.month()),r=t.clone().add(n,"months"),i=e-r<0,o=t.clone().add(n+(i?-1:1),"months");return Number(-(n+(e-r)/(i?r-o:o-r)))},absFloor:function(t){return t<0?Math.ceil(t)||0:Math.floor(t)},prettyUnit:function(c){return{M:a,y:s,w:o,d:i,h:r,m:n,s:e,ms:t}[c]||String(c||"").toLowerCase().replace(/s$/,"")},isUndefined:function(t){return void 0===t}},d="en",h={};h[d]=l;var v=function(t){return t instanceof _},m=function(t,e,n){var r;if(!t)return null;if("string"==typeof t)h[t]&&(r=t),e&&(h[t]=e,r=t);else{var i=t.name;h[i]=t,r=i}return n||(d=r),r},y=function(t,e){if(v(t))return t.clone();var n=e||{};return n.date=t,new _(n)},g=function(t,e){return y(t,{locale:e.$L})},b=p;b.parseLocale=m,b.isDayjs=v,b.wrapper=g;var _=function(){function l(t){this.parse(t)}var f=l.prototype;return f.parse=function(t){var e,n;this.$d=null===(e=t.date)?new Date(NaN):b.isUndefined(e)?new Date:e instanceof Date?e:"string"==typeof e&&/.*[^Z]$/i.test(e)&&(n=e.match(c))?new Date(n[1],n[2]-1,n[3]||1,n[5]||0,n[6]||0,n[7]||0,n[8]||0):new Date(e),this.init(t)},f.init=function(t){var e=this.$d;this.$y=e.getFullYear(),this.$M=e.getMonth(),this.$D=e.getDate(),this.$W=e.getDay(),this.$H=e.getHours(),this.$m=e.getMinutes(),this.$s=e.getSeconds(),this.$ms=e.getMilliseconds(),this.$L=this.$L||m(t.locale,null,!0)||d},f.$utils=function(){return b},f.isValid=function(){return!("Invalid Date"===this.$d.toString())},f.isSame=function(t,e){var n=y(t);return this.startOf(e)<=n&&n<=this.endOf(e)},f.isAfter=function(t,e){return y(t)-1?t.replace(/\[|\]/g,""):{YY:String(e.$y).slice(-2),YYYY:String(e.$y),M:String(e.$M+1),MM:b.padStart(e.$M+1,2,"0"),MMM:s(i.monthsShort,e.$M,a,3),MMMM:a[e.$M],D:String(e.$D),DD:b.padStart(e.$D,2,"0"),d:String(e.$W),dd:s(i.weekdaysMin,e.$W,o,2),ddd:s(i.weekdaysShort,e.$W,o,3),dddd:o[e.$W],H:String(e.$H),HH:b.padStart(e.$H,2,"0"),h:c(t),hh:c(t),a:e.$H<12?"am":"pm",A:e.$H<12?"AM":"PM",m:String(e.$m),mm:b.padStart(e.$m,2,"0"),s:String(e.$s),ss:b.padStart(e.$s,2,"0"),SSS:b.padStart(e.$ms,3,"0"),Z:r}[t]||r.replace(":","")})},f.diff=function(t,c,u){var l,f=b.prettyUnit(c),p=y(t),d=this-p,h=b.monthDiff(this,p);return h=(l={},l[s]=h/12,l[a]=h,l.quarter=h/3,l[o]=d/6048e5,l[i]=d/864e5,l[r]=d/36e5,l[n]=d/6e4,l[e]=d/1e3,l)[f]||d,u?h:b.absFloor(h)},f.daysInMonth=function(){return this.endOf(a).$D},f.$locale=function(){return h[this.$L]},f.locale=function(t,e){var n=this.clone();return n.$L=m(t,e,!0),n},f.clone=function(){return g(this.toDate(),this)},f.toDate=function(){return new Date(this.$d)},f.toArray=function(){return[this.$y,this.$M,this.$D,this.$H,this.$m,this.$s,this.$ms]},f.toJSON=function(){return this.toISOString()},f.toISOString=function(){return this.$d.toISOString()},f.toObject=function(){return{years:this.$y,months:this.$M,date:this.$D,hours:this.$H,minutes:this.$m,seconds:this.$s,milliseconds:this.$ms}},f.toString=function(){return this.$d.toUTCString()},l}();return y.extend=function(t,e){return t(e,_,y),y},y.locale=m,y.isDayjs=v,y.unix=function(t){return y(1e3*t)},y.en=h[d],y})},"5ca1":function(t,e,n){var r=n("7726"),i=n("8378"),o=n("32e9"),a=n("2aba"),s=n("9b43"),c="prototype",u=function(t,e,n){var l,f,p,d,h=t&u.F,v=t&u.G,m=t&u.S,y=t&u.P,g=t&u.B,b=v?r:m?r[e]||(r[e]={}):(r[e]||{})[c],_=v?i:i[e]||(i[e]={}),w=_[c]||(_[c]={});for(l in v&&(n=e),n)f=!h&&b&&void 0!==b[l],p=(f?b:n)[l],d=g&&f?s(p,r):y&&"function"==typeof p?s(Function.call,p):p,b&&a(b,l,p,t&u.U),_[l]!=p&&o(_,l,d),y&&w[l]!=p&&(w[l]=p)};r.core=i,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},"5cc5":function(t,e,n){var r=n("2b4c")("iterator"),i=!1;try{var o=[7][r]();o["return"]=function(){i=!0},Array.from(o,function(){throw 2})}catch(a){}t.exports=function(t,e){if(!e&&!i)return!1;var n=!1;try{var o=[7],s=o[r]();s.next=function(){return{done:n=!0}},o[r]=function(){return s},t(o)}catch(a){}return n}},"5d75":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef"),i=/(^$|^(([^<>()[\]\\.,;:\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",i)},"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"),i=n("8b97").set;t.exports=function(t,e,n){var o,a=e.constructor;return a!==n&&"function"==typeof a&&(o=a.prototype)!==n.prototype&&r(o)&&i&&i(t,o),t}},"5eda":function(t,e,n){var r=n("5ca1"),i=n("8378"),o=n("79e5");t.exports=function(t,e){var n=(i.Object||{})[t]||Object[t],a={};a[t]=e(n),r(r.S+r.F*o(function(){n(1)}),"Object",a)}},"5f1b":function(t,e,n){"use strict";var r=n("23c6"),i=RegExp.prototype.exec;t.exports=function(t,e){var n=t.exec;if("function"===typeof n){var o=n.call(t,e);if("object"!==typeof o)throw new TypeError("RegExp exec method returned something other than an Object or null");return o}if("RegExp"!==r(t))throw new TypeError("RegExp#exec called on incompatible receiver");return i.call(t,e)}},"613b":function(t,e,n){var r=n("5537")("keys"),i=n("ca5a");t.exports=function(t){return r[t]||(r[t]=i(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)}},6762:function(t,e,n){"use strict";var r=n("5ca1"),i=n("c366")(!0);r(r.P,"Array",{includes:function(t){return i(this,t,arguments.length>1?arguments[1]:void 0)}}),n("9c6c")("includes")},6821:function(t,e,n){var r=n("626a"),i=n("be13");t.exports=function(t){return r(i(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,i;if(e&&"function"==typeof(n=t.toString)&&!r(i=n.call(t)))return i;if("function"==typeof(n=t.valueOf)&&!r(i=n.call(t)))return i;if(!e&&"function"==typeof(n=t.toString)&&!r(i=n.call(t)))return i;throw TypeError("Can't convert object to primitive value")}},"6b54":function(t,e,n){"use strict";n("3846");var r=n("cb7c"),i=n("0bfb"),o=n("9e1e"),a="toString",s=/./[a],c=function(t){n("2aba")(RegExp.prototype,a,t,!0)};n("79e5")(function(){return"/a/b"!=s.call({source:"a",flags:"b"})})?c(function(){var t=r(this);return"/".concat(t.source,"/","flags"in t?t.flags:!o&&t instanceof RegExp?i.call(t):void 0)}):s.name!=a&&c(function(){return s.call(this)})},7333:function(t,e,n){"use strict";var r=n("0d58"),i=n("2621"),o=n("52a7"),a=n("4bf8"),s=n("626a"),c=Object.assign;t.exports=!c||n("79e5")(function(){var t={},e={},n=Symbol(),r="abcdefghijklmnopqrst";return t[n]=7,r.split("").forEach(function(t){e[t]=t}),7!=c({},t)[n]||Object.keys(c({},e)).join("")!=r})?function(t,e){var n=a(t),c=arguments.length,u=1,l=i.f,f=o.f;while(c>u){var p,d=s(arguments[u++]),h=l?r(d).concat(l(d)):r(d),v=h.length,m=0;while(v>m)f.call(d,p=h[m++])&&(n[p]=d[p])}return n}:c},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"),i=/^(?:(?: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",i)},"77f1":function(t,e,n){var r=n("4588"),i=Math.max,o=Math.min;t.exports=function(t,e){return t=r(t),t<0?i(t+e,0):o(t,e)}},"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"),i=o(r);function o(t){return t&&t.__esModule?t:{default:t}}e.withParams=i.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,i.default)({type:t},function(t){return!a(t)||e.test(t)})}},"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"),i=n("86cc"),o=n("9e1e"),a=n("2b4c")("species");t.exports=function(t){var e=r[t];o&&e&&!e[a]&&i.f(e,a,{configurable:!0,get:function(){return this}})}},"7f20":function(t,e,n){var r=n("86cc").f,i=n("69a8"),o=n("2b4c")("toStringTag");t.exports=function(t,e,n){t&&!i(t=n?t:t.prototype,o)&&r(t,o,{configurable:!0,value:e})}},"7f7f":function(t,e,n){var r=n("86cc").f,i=Function.prototype,o=/^\s*function ([^ (]*)/,a="name";a in i||n("9e1e")&&r(i,a,{configurable:!0,get:function(){try{return(""+this).match(o)[1]}catch(t){return""}}})},8079:function(t,e,n){var r=n("7726"),i=n("1991").set,o=r.MutationObserver||r.WebKitMutationObserver,a=r.process,s=r.Promise,c="process"==n("2d95")(a);t.exports=function(){var t,e,n,u=function(){var r,i;c&&(r=a.domain)&&r.exit();while(t){i=t.fn,t=t.next;try{i()}catch(o){throw t?n():e=void 0,o}}e=void 0,r&&r.enter()};if(c)n=function(){a.nextTick(u)};else if(!o||r.navigator&&r.navigator.standalone)if(s&&s.resolve){var l=s.resolve(void 0);n=function(){l.then(u)}}else n=function(){i.call(r,u)};else{var f=!0,p=document.createTextNode("");new o(u).observe(p,{characterData:!0}),n=function(){p.data=f=!f}}return function(r){var i={fn:r,next:void 0};e&&(e.next=i),t||(t=i,n()),e=i}}},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={}},8615:function(t,e,n){var r=n("5ca1"),i=n("504c")(!1);r(r.S,"Object",{values:function(t){return i(t)}})},"86cc":function(t,e,n){var r=n("cb7c"),i=n("c69a"),o=n("6a99"),a=Object.defineProperty;e.f=n("9e1e")?Object.defineProperty:function(t,e,n){if(r(t),e=o(e,!0),r(n),i)try{return a(t,e,n)}catch(s){}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},"8b97":function(t,e,n){var r=n("d3f4"),i=n("cb7c"),o=function(t,e){if(i(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(i){e=!0}return function(t,n){return o(t,n),e?t.__proto__=n:r(t,n),t}}({},!1):void 0),check:o}},"8c4f":function(t,e,n){"use strict"; +/*! + * vue-router v3.0.2 + * (c) 2018 Evan You + * @license MIT + */function r(t,e){0}function i(t){return Object.prototype.toString.call(t).indexOf("Error")>-1}function o(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,i=e.parent,a=e.data;a.routerView=!0;var c=i.$createElement,u=n.name,l=i.$route,f=i._routerViewCache||(i._routerViewCache={}),p=0,d=!1;while(i&&i._routerRoot!==i)i.$vnode&&i.$vnode.data.routerView&&p++,i._inactive&&(d=!0),i=i.$parent;if(a.routerViewDepth=p,d)return c(f[u],a,r);var h=l.matched[p];if(!h)return f[u]=null,c();var v=f[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 m=a.props=s(l,h.props&&h.props[u]);if(m){m=a.props=o({},m);var y=a.attrs=a.attrs||{};for(var g in m)v.props&&g in v.props||(y[g]=m[g],delete m[g])}return c(v,a,r)}};function s(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 c=/[!'()*]/g,u=function(t){return"%"+t.charCodeAt(0).toString(16)},l=/%2C/g,f=function(t){return encodeURIComponent(t).replace(c,u).replace(l,",")},p=decodeURIComponent;function d(t,e,n){void 0===e&&(e={});var r,i=n||h;try{r=i(t||"")}catch(a){r={}}for(var o in e)r[o]=e[o];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()),i=n.length>0?p(n.join("=")):null;void 0===e[r]?e[r]=i:Array.isArray(e[r])?e[r].push(i):e[r]=[e[r],i]}),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 f(e);if(Array.isArray(n)){var r=[];return n.forEach(function(t){void 0!==t&&(null===t?r.push(f(e)):r.push(f(e)+"="+f(t)))}),r.join("&")}return f(e)+"="+f(n)}).filter(function(t){return t.length>0}).join("&"):null;return e?"?"+e:""}var m=/\/?$/;function y(t,e,n,r){var i=r&&r.options.stringifyQuery,o=e.query||{};try{o=g(o)}catch(s){}var a={name:e.name||t&&t.name,meta:t&&t.meta||{},path:e.path||"/",hash:e.hash||"",query:o,params:e.params||{},fullPath:w(e,i),matched:t?_(t):[]};return n&&(a.redirectedFrom=w(n,i)),Object.freeze(a)}function g(t){if(Array.isArray(t))return t.map(g);if(t&&"object"===typeof t){var e={};for(var n in t)e[n]=g(t[n]);return e}return t}var b=y(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 i=t.hash;void 0===i&&(i="");var o=e||v;return(n||"/")+o(r)+i}function x(t,e){return e===b?t===e:!!e&&(t.path&&e.path?t.path.replace(m,"")===e.path.replace(m,"")&&t.hash===e.hash&&$(t.query,e.query):!(!t.name||!e.name)&&(t.name===e.name&&t.hash===e.hash&&$(t.query,e.query)&&$(t.params,e.params)))}function $(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],i=e[n];return"object"===typeof r&&"object"===typeof i?$(r,i):String(r)===String(i)})}function O(t,e){return 0===t.path.replace(m,"/").indexOf(e.path.replace(m,"/"))&&(!e.hash||t.hash===e.hash)&&C(t.query,e.query)}function C(t,e){for(var n in e)if(!(n in t))return!1;return!0}var S,k=[String,Object],A=[String,Array],E={name:"RouterLink",props:{to:{type:k,required:!0},tag:{type:String,default:"a"},exact:Boolean,append:Boolean,replace:Boolean,activeClass:String,exactActiveClass:String,event:{type:A,default:"click"}},render:function(t){var e=this,n=this.$router,r=this.$route,i=n.resolve(this.to,r,this.append),a=i.location,s=i.route,c=i.href,u={},l=n.options.linkActiveClass,f=n.options.linkExactActiveClass,p=null==l?"router-link-active":l,d=null==f?"router-link-exact-active":f,h=null==this.activeClass?p:this.activeClass,v=null==this.exactActiveClass?d:this.exactActiveClass,m=a.path?y(null,a,null,n):s;u[v]=x(r,m),u[h]=this.exact?u[v]:O(r,m);var g=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]=g}):b[this.event]=g;var _={class:u};if("a"===this.tag)_.on=b,_.attrs={href:c};else{var w=T(this.$slots.default);if(w){w.isStatic=!1;var $=w.data=o({},w.data);$.on=b;var C=w.data.attrs=o({},w.data.attrs);C.href=c}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 T(t){if(t)for(var e,n=0;n=0&&(e=t.slice(r),t=t.slice(0,r));var i=t.indexOf("?");return i>=0&&(n=t.slice(i+1),t=t.slice(0,i)),{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=V,U=q,z=W,H=nt,B=new RegExp(["(\\\\.)","([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))"].join("|"),"g");function V(t,e){var n,r=[],i=0,o=0,a="",s=e&&e.delimiter||"/";while(null!=(n=B.exec(t))){var c=n[0],u=n[1],l=n.index;if(a+=t.slice(o,l),o=l+c.length,u)a+=u[1];else{var f=t[o],p=n[2],d=n[3],h=n[4],v=n[5],m=n[6],y=n[7];a&&(r.push(a),a="");var g=null!=p&&null!=f&&f!==p,b="+"===m||"*"===m,_="?"===m||"*"===m,w=n[2]||s,x=h||v;r.push({name:d||i++,prefix:p||"",delimiter:w,optional:_,repeat:b,partial:g,asterisk:!!y,pattern:x?X(x):y?".*":"[^"+G(w)+"]+?"})}}return o-1&&(s.params[p]=n.params[p]);if(u)return s.path=ot(u.path,s.params,'named route "'+c+'"'),l(u,s,a)}else if(s.path){s.params={};for(var d=0;d=t.length?n():t[i]?e(t[i],function(){r(i+1)}):r(i+1)};r(0)}function Dt(t){return function(e,n,r){var o=!1,a=0,s=null;Lt(t,function(t,e,n,c){if("function"===typeof t&&void 0===t.cid){o=!0,a++;var u,l=Ft(function(e){Rt(e)&&(e=e.default),t.resolved="function"===typeof e?e:S.extend(e),n.components[c]=e,a--,a<=0&&r()}),f=Ft(function(t){var e="Failed to resolve async component "+c+": "+t;s||(s=i(t)?t:new Error(e),r(s))});try{u=t(l,f)}catch(d){f(d)}if(u)if("function"===typeof u.then)u.then(l,f);else{var p=u.component;p&&"function"===typeof p.then&&p.then(l,f)}}}),o||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 Ut=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 Ht(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 ie(t){Ct?Tt(re(t)):window.location.hash=t}function oe(t){Ct?jt(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}(Ut),se=function(t){void 0===t&&(t={}),this.app=null,this.apps=[],this.options=t,this.beforeHooks=[],this.resolveHooks=[],this.afterHooks=[],this.matcher=ft(t.routes||[],this);var e=t.mode||"hash";switch(this.fallback="history"===e&&!Ct&&!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}},ce={currentRoute:{configurable:!0}};function ue(t,e){return t.push(e),function(){var n=t.indexOf(e);n>-1&&t.splice(n,1)}}function le(t,e,n){var r="hash"===n?"#"+e:e;return t?I(t+"/"+r):r}se.prototype.match=function(t,e,n){return this.matcher.match(t,e,n)},ce.currentRoute.get=function(){return this.history&&this.history.current},se.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})})}},se.prototype.beforeEach=function(t){return ue(this.beforeHooks,t)},se.prototype.beforeResolve=function(t){return ue(this.resolveHooks,t)},se.prototype.afterEach=function(t){return ue(this.afterHooks,t)},se.prototype.onReady=function(t,e){this.history.onReady(t,e)},se.prototype.onError=function(t){this.history.onError(t)},se.prototype.push=function(t,e,n){this.history.push(t,e,n)},se.prototype.replace=function(t,e,n){this.history.replace(t,e,n)},se.prototype.go=function(t){this.history.go(t)},se.prototype.back=function(){this.go(-1)},se.prototype.forward=function(){this.go(1)},se.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]})})):[]},se.prototype.resolve=function(t,e,n){var r=lt(t,e||this.history.current,n,this),i=this.match(r,e),o=i.redirectedFrom||i.fullPath,a=this.history.base,s=le(a,o,this.mode);return{location:r,route:i,href:s,normalizedTo:r,resolved:i}},se.prototype.addRoutes=function(t){this.matcher.addRoutes(t),this.history.current!==b&&this.history.transitionTo(this.history.getCurrentLocation())},Object.defineProperties(se.prototype,ce),se.install=j,se.version="3.0.2",P&&window.Vue&&window.Vue.use(se),e["a"]=se},9093:function(t,e,n){var r=n("ce10"),i=n("e11e").concat("length","prototype");e.f=Object.getOwnPropertyNames||function(t){return r(t,i)}},"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(i)})};var i=function(t){return t.toLowerCase().match(/^[0-9a-f]{2}$/)}},"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,i){return t.call(e,n,r,i)}}return function(){return t.apply(e,arguments)}}},"9c6c":function(t,e,n){var r=n("2b4c")("unscopables"),i=Array.prototype;void 0==i[r]&&n("32e9")(i,r,{}),t.exports=function(t){i[r][t]=!0}},"9c80":function(t,e){t.exports=function(t){try{return{e:!1,v:t()}}catch(e){return{e:!0,v:e}}}},"9def":function(t,e,n){var r=n("4588"),i=Math.min;t.exports=function(t){return t>0?i(r(t),9007199254740991):0}},"9e1e":function(t,e,n){t.exports=!n("79e5")(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},a026:function(t,e,n){"use strict";(function(t){ +/*! + * Vue.js v2.5.21 + * (c) 2014-2018 Evan You + * Released under the MIT License. + */ +var n=Object.freeze({});function r(t){return void 0===t||null===t}function i(t){return void 0!==t&&null!==t}function o(t){return!0===t}function a(t){return!1===t}function s(t){return"string"===typeof t||"number"===typeof t||"symbol"===typeof t||"boolean"===typeof t}function c(t){return null!==t&&"object"===typeof t}var u=Object.prototype.toString;function l(t){return"[object Object]"===u.call(t)}function f(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 null==t?"":"object"===typeof t?JSON.stringify(t,null,2):String(t)}function h(t){var e=parseFloat(t);return isNaN(e)?t:e}function v(t,e){for(var n=Object.create(null),r=t.split(","),i=0;i-1)return t.splice(n,1)}}var b=Object.prototype.hasOwnProperty;function _(t,e){return b.call(t,e)}function w(t){var e=Object.create(null);return function(n){var r=e[n];return r||(e[n]=t(n))}}var x=/-(\w)/g,$=w(function(t){return t.replace(x,function(t,e){return e?e.toUpperCase():""})}),O=w(function(t){return t.charAt(0).toUpperCase()+t.slice(1)}),C=/\B([A-Z])/g,S=w(function(t){return t.replace(C,"-$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 A(t,e){return t.bind(e)}var E=Function.prototype.bind?A:k;function M(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 j(t){for(var e={},n=0;n0,nt=Q&&Q.indexOf("edge/")>0,rt=(Q&&Q.indexOf("android"),Q&&/iphone|ipad|ipod|ios/.test(Q)||"ios"===Z),it=(Q&&/chrome\/\d+/.test(Q),{}.watch),ot=!1;if(X)try{var at={};Object.defineProperty(at,"passive",{get:function(){ot=!0}}),window.addEventListener("test-passive",null,at)}catch(wu){}var st=function(){return void 0===W&&(W=!X&&!J&&"undefined"!==typeof t&&(t["process"]&&"server"===t["process"].env.VUE_ENV)),W},ct=X&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__;function ut(t){return"function"===typeof t&&/native code/.test(t.toString())}var lt,ft="undefined"!==typeof Symbol&&ut(Symbol)&&"undefined"!==typeof Reflect&&ut(Reflect.ownKeys);lt="undefined"!==typeof Set&&ut(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 pt=P,dt=0,ht=function(){this.id=dt++,this.subs=[]};ht.prototype.addSub=function(t){this.subs.push(t)},ht.prototype.removeSub=function(t){g(this.subs,t)},ht.prototype.depend=function(){ht.target&&ht.target.addDep(this)},ht.prototype.notify=function(){var t=this.subs.slice();for(var e=0,n=t.length;e-1)if(o&&!_(i,"default"))a=!1;else if(""===a||a===S(t)){var c=Zt(String,i.type);(c<0||s0&&(a=Se(a,(e||"")+"_"+n),Ce(a[0])&&Ce(u)&&(l[c]=wt(u.text+a[0].text),a.shift()),l.push.apply(l,a)):s(a)?Ce(u)?l[c]=wt(u.text+a):""!==a&&l.push(wt(a)):Ce(a)&&Ce(u)?l[c]=wt(u.text+a.text):(o(t._isVList)&&i(a.tag)&&r(a.key)&&i(e)&&(a.key="__vlist"+e+"_"+n+"__"),l.push(a)));return l}function ke(t,e){return(t.__esModule||ft&&"Module"===t[Symbol.toStringTag])&&(t=t.default),c(t)?e.extend(t):t}function Ae(t,e,n,r,i){var o=_t();return o.asyncFactory=t,o.asyncMeta={data:e,context:n,children:r,tag:i},o}function Ee(t,e,n){if(o(t.error)&&i(t.errorComp))return t.errorComp;if(i(t.resolved))return t.resolved;if(o(t.loading)&&i(t.loadingComp))return t.loadingComp;if(!i(t.contexts)){var a=t.contexts=[n],s=!0,u=function(t){for(var e=0,n=a.length;e1?M(n):n;for(var r=M(arguments,1),i=0,o=n.length;inn&&Je[n].id>t.id)n--;Je.splice(n+1,0,t)}else Je.push(t);tn||(tn=!0,pe(on))}}var ln=0,fn=function(t,e,n,r,i){this.vm=t,i&&(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=++ln,this.active=!0,this.dirty=this.lazy,this.deps=[],this.newDeps=[],this.depIds=new lt,this.newDepIds=new lt,this.expression="","function"===typeof e?this.getter=e:(this.getter=K(e),this.getter||(this.getter=P)),this.value=this.lazy?void 0:this.get()};fn.prototype.get=function(){var t;mt(this);var e=this.vm;try{t=this.getter.call(e,e)}catch(wu){if(!this.user)throw wu;Qt(wu,e,'getter for watcher "'+this.expression+'"')}finally{this.deep&&he(t),yt(),this.cleanupDeps()}return t},fn.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))},fn.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},fn.prototype.update=function(){this.lazy?this.dirty=!0:this.sync?this.run():un(this)},fn.prototype.run=function(){if(this.active){var t=this.get();if(t!==this.value||c(t)||this.deep){var e=this.value;if(this.value=t,this.user)try{this.cb.call(this.vm,t,e)}catch(wu){Qt(wu,this.vm,'callback for watcher "'+this.expression+'"')}else this.cb.call(this.vm,t,e)}}},fn.prototype.evaluate=function(){this.value=this.get(),this.dirty=!1},fn.prototype.depend=function(){var t=this.deps.length;while(t--)this.deps[t].depend()},fn.prototype.teardown=function(){if(this.active){this.vm._isBeingDestroyed||g(this.vm._watchers,this);var t=this.deps.length;while(t--)this.deps[t].removeSub(this);this.active=!1}};var pn={enumerable:!0,configurable:!0,get:P,set:P};function dn(t,e,n){pn.get=function(){return this[e][n]},pn.set=function(t){this[e][n]=t},Object.defineProperty(t,n,pn)}function hn(t){t._watchers=[];var e=t.$options;e.props&&vn(t,e.props),e.methods&&$n(t,e.methods),e.data?mn(t):jt(t._data={},!0),e.computed&&bn(t,e.computed),e.watch&&e.watch!==it&&On(t,e.watch)}function vn(t,e){var n=t.$options.propsData||{},r=t._props={},i=t.$options._propKeys=[],o=!t.$parent;o||At(!1);var a=function(o){i.push(o);var a=Wt(o,e,n,t);Pt(r,o,a),o in t||dn(t,"_props",o)};for(var s in e)a(s);At(!0)}function mn(t){var e=t.$options.data;e=t._data="function"===typeof e?yn(e,t):e||{},l(e)||(e={});var n=Object.keys(e),r=t.$options.props,i=(t.$options.methods,n.length);while(i--){var o=n[i];0,r&&_(r,o)||V(o)||dn(t,"_data",o)}jt(e,!0)}function yn(t,e){mt();try{return t.call(e,e)}catch(wu){return Qt(wu,e,"data()"),{}}finally{yt()}}var gn={lazy:!0};function bn(t,e){var n=t._computedWatchers=Object.create(null),r=st();for(var i in e){var o=e[i],a="function"===typeof o?o:o.get;0,r||(n[i]=new fn(t,a||P,P,gn)),i in t||_n(t,i,o)}}function _n(t,e,n){var r=!st();"function"===typeof n?(pn.get=r?wn(e):xn(n),pn.set=P):(pn.get=n.get?r&&!1!==n.cache?wn(e):xn(n.get):P,pn.set=n.set||P),Object.defineProperty(t,e,pn)}function wn(t){return function(){var e=this._computedWatchers&&this._computedWatchers[t];if(e)return e.dirty&&e.evaluate(),ht.target&&e.depend(),e.value}}function xn(t){return function(){return t.call(this,this)}}function $n(t,e){t.$options.props;for(var n in e)t[n]="function"!==typeof e[n]?P:E(e[n],t)}function On(t,e){for(var n in e){var r=e[n];if(Array.isArray(r))for(var i=0;i=0||n.indexOf(t[i])<0)&&r.push(t[i]);return r}return t}function dr(t){this._init(t)}function hr(t){t.use=function(t){var e=this._installedPlugins||(this._installedPlugins=[]);if(e.indexOf(t)>-1)return this;var n=M(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 vr(t){t.mixin=function(t){return this.options=Yt(this.options,t),this}}function mr(t){t.cid=0;var e=1;t.extend=function(t){t=t||{};var n=this,r=n.cid,i=t._Ctor||(t._Ctor={});if(i[r])return i[r];var o=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=Yt(n.options,t),a["super"]=n,a.options.props&&yr(a),a.options.computed&&gr(a),a.extend=n.extend,a.mixin=n.mixin,a.use=n.use,z.forEach(function(t){a[t]=n[t]}),o&&(a.options.components[o]=a),a.superOptions=n.options,a.extendOptions=t,a.sealedOptions=T({},a.options),i[r]=a,a}}function yr(t){var e=t.options.props;for(var n in e)dn(t.prototype,"_props",n)}function gr(t){var e=t.options.computed;for(var n in e)_n(t.prototype,n,e[n])}function br(t){z.forEach(function(e){t[e]=function(t,n){return n?("component"===e&&l(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 _r(t){return t&&(t.Ctor.options.name||t.tag)}function wr(t,e){return Array.isArray(t)?t.indexOf(e)>-1:"string"===typeof t?t.split(",").indexOf(e)>-1:!!f(t)&&t.test(e)}function xr(t,e){var n=t.cache,r=t.keys,i=t._vnode;for(var o in n){var a=n[o];if(a){var s=_r(a.componentOptions);s&&!e(s)&&$r(n,o,r,i)}}}function $r(t,e,n,r){var i=t[e];!i||r&&i.tag===r.tag||i.componentInstance.$destroy(),t[e]=null,g(n,e)}cr(dr),Sn(dr),Ne(dr),Ve(dr),ar(dr);var Or=[String,RegExp,Array],Cr={name:"keep-alive",abstract:!0,props:{include:Or,exclude:Or,max:[String,Number]},created:function(){this.cache=Object.create(null),this.keys=[]},destroyed:function(){for(var t in this.cache)$r(this.cache,t,this.keys)},mounted:function(){var t=this;this.$watch("include",function(e){xr(t,function(t){return wr(e,t)})}),this.$watch("exclude",function(e){xr(t,function(t){return!wr(e,t)})})},render:function(){var t=this.$slots.default,e=Te(t),n=e&&e.componentOptions;if(n){var r=_r(n),i=this,o=i.include,a=i.exclude;if(o&&(!r||!wr(o,r))||a&&r&&wr(a,r))return e;var s=this,c=s.cache,u=s.keys,l=null==e.key?n.Ctor.cid+(n.tag?"::"+n.tag:""):e.key;c[l]?(e.componentInstance=c[l].componentInstance,g(u,l),u.push(l)):(c[l]=e,u.push(l),this.max&&u.length>parseInt(this.max)&&$r(c,u[0],u,this._vnode)),e.data.keepAlive=!0}return e||t&&t[0]}},Sr={KeepAlive:Cr};function kr(t){var e={get:function(){return B}};Object.defineProperty(t,"config",e),t.util={warn:pt,extend:T,mergeOptions:Yt,defineReactive:Pt},t.set=Dt,t.delete=Lt,t.nextTick=pe,t.options=Object.create(null),z.forEach(function(e){t.options[e+"s"]=Object.create(null)}),t.options._base=t,T(t.options.components,Sr),hr(t),vr(t),mr(t),br(t)}kr(dr),Object.defineProperty(dr.prototype,"$isServer",{get:st}),Object.defineProperty(dr.prototype,"$ssrContext",{get:function(){return this.$vnode&&this.$vnode.ssrContext}}),Object.defineProperty(dr,"FunctionalRenderContext",{value:Hn}),dr.version="2.5.21";var Ar=v("style,class"),Er=v("input,textarea,option,select,progress"),Mr=function(t,e,n){return"value"===n&&Er(t)&&"button"!==e||"selected"===n&&"option"===t||"checked"===n&&"input"===t||"muted"===n&&"video"===t},Tr=v("contenteditable,draggable,spellcheck"),jr=v("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"),Pr="http://www.w3.org/1999/xlink",Dr=function(t){return":"===t.charAt(5)&&"xlink"===t.slice(0,5)},Lr=function(t){return Dr(t)?t.slice(6,t.length):""},Ir=function(t){return null==t||!1===t};function Nr(t){var e=t.data,n=t,r=t;while(i(r.componentInstance))r=r.componentInstance._vnode,r&&r.data&&(e=Rr(r.data,e));while(i(n=n.parent))n&&n.data&&(e=Rr(e,n.data));return Fr(e.staticClass,e.class)}function Rr(t,e){return{staticClass:Ur(t.staticClass,e.staticClass),class:i(t.class)?[t.class,e.class]:e.class}}function Fr(t,e){return i(t)||i(e)?Ur(t,zr(e)):""}function Ur(t,e){return t?e?t+" "+e:t:e||""}function zr(t){return Array.isArray(t)?Hr(t):c(t)?Br(t):"string"===typeof t?t:""}function Hr(t){for(var e,n="",r=0,o=t.length;r-1?Xr[t]=e.constructor===window.HTMLUnknownElement||e.constructor===window.HTMLElement:Xr[t]=/HTMLUnknownElement/.test(e.toString())}var Zr=v("text,number,password,search,email,tel,url");function Qr(t){if("string"===typeof t){var e=document.querySelector(t);return e||document.createElement("div")}return t}function ti(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 ei(t,e){return document.createElementNS(Vr[t],e)}function ni(t){return document.createTextNode(t)}function ri(t){return document.createComment(t)}function ii(t,e,n){t.insertBefore(e,n)}function oi(t,e){t.removeChild(e)}function ai(t,e){t.appendChild(e)}function si(t){return t.parentNode}function ci(t){return t.nextSibling}function ui(t){return t.tagName}function li(t,e){t.textContent=e}function fi(t,e){t.setAttribute(e,"")}var pi=Object.freeze({createElement:ti,createElementNS:ei,createTextNode:ni,createComment:ri,insertBefore:ii,removeChild:oi,appendChild:ai,parentNode:si,nextSibling:ci,tagName:ui,setTextContent:li,setStyleScope:fi}),di={create:function(t,e){hi(e)},update:function(t,e){t.data.ref!==e.data.ref&&(hi(t,!0),hi(e))},destroy:function(t){hi(t,!0)}};function hi(t,e){var n=t.data.ref;if(i(n)){var r=t.context,o=t.componentInstance||t.elm,a=r.$refs;e?Array.isArray(a[n])?g(a[n],o):a[n]===o&&(a[n]=void 0):t.data.refInFor?Array.isArray(a[n])?a[n].indexOf(o)<0&&a[n].push(o):a[n]=[o]:a[n]=o}}var vi=new gt("",{},[]),mi=["create","activate","update","remove","destroy"];function yi(t,e){return t.key===e.key&&(t.tag===e.tag&&t.isComment===e.isComment&&i(t.data)===i(e.data)&&gi(t,e)||o(t.isAsyncPlaceholder)&&t.asyncFactory===e.asyncFactory&&r(e.asyncFactory.error))}function gi(t,e){if("input"!==t.tag)return!0;var n,r=i(n=t.data)&&i(n=n.attrs)&&n.type,o=i(n=e.data)&&i(n=n.attrs)&&n.type;return r===o||Zr(r)&&Zr(o)}function bi(t,e,n){var r,o,a={};for(r=e;r<=n;++r)o=t[r].key,i(o)&&(a[o]=r);return a}function _i(t){var e,n,a={},c=t.modules,u=t.nodeOps;for(e=0;ev?(f=r(n[g+1])?null:n[g+1].elm,$(t,f,n,h,g,o)):h>g&&C(t,e,p,v)}function A(t,e,n,r){for(var o=n;o-1?Ti(t,e,n):jr(e)?Ir(n)?t.removeAttribute(e):(n="allowfullscreen"===e&&"EMBED"===t.tagName?"true":e,t.setAttribute(e,n)):Tr(e)?t.setAttribute(e,Ir(n)||"false"===n?"false":"true"):Dr(e)?Ir(n)?t.removeAttributeNS(Pr,Lr(e)):t.setAttributeNS(Pr,e,n):Ti(t,e,n)}function Ti(t,e,n){if(Ir(n))t.removeAttribute(e);else{if(tt&&!et&&("TEXTAREA"===t.tagName||"INPUT"===t.tagName)&&"placeholder"===e&&!t.__ieph){var r=function(e){e.stopImmediatePropagation(),t.removeEventListener("input",r)};t.addEventListener("input",r),t.__ieph=!0}t.setAttribute(e,n)}}var ji={create:Ei,update:Ei};function Pi(t,e){var n=e.elm,o=e.data,a=t.data;if(!(r(o.staticClass)&&r(o.class)&&(r(a)||r(a.staticClass)&&r(a.class)))){var s=Nr(e),c=n._transitionClasses;i(c)&&(s=Ur(s,zr(c))),s!==n._prevClass&&(n.setAttribute("class",s),n._prevClass=s)}}var Di,Li,Ii,Ni,Ri,Fi,Ui={create:Pi,update:Pi},zi=/[\w).+\-_$\]]/;function Hi(t){var e,n,r,i,o,a=!1,s=!1,c=!1,u=!1,l=0,f=0,p=0,d=0;for(r=0;r=0;h--)if(v=t.charAt(h)," "!==v)break;v&&zi.test(v)||(u=!0)}}else void 0===i?(d=r+1,i=t.slice(0,r).trim()):m();function m(){(o||(o=[])).push(t.slice(d,r).trim()),d=r+1}if(void 0===i?i=t.slice(0,r).trim():0!==d&&m(),o)for(r=0;r-1?{exp:t.slice(0,Ni),key:'"'+t.slice(Ni+1)+'"'}:{exp:t,key:null};Li=t,Ni=Ri=Fi=0;while(!ro())Ii=no(),io(Ii)?ao(Ii):91===Ii&&oo(Ii);return{exp:t.slice(0,Ri),key:t.slice(Ri+1,Fi)}}function no(){return Li.charCodeAt(++Ni)}function ro(){return Ni>=Di}function io(t){return 34===t||39===t}function oo(t){var e=1;Ri=Ni;while(!ro())if(t=no(),io(t))ao(t);else if(91===t&&e++,93===t&&e--,0===e){Fi=Ni;break}}function ao(t){var e=t;while(!ro())if(t=no(),t===e)break}var so,co="__r",uo="__c";function lo(t,e,n){n;var r=e.value,i=e.modifiers,o=t.tag,a=t.attrsMap.type;if(t.component)return Qi(t,r,i),!1;if("select"===o)ho(t,r,i);else if("input"===o&&"checkbox"===a)fo(t,r,i);else if("input"===o&&"radio"===a)po(t,r,i);else if("input"===o||"textarea"===o)vo(t,r,i);else{if(!B.isReservedTag(o))return Qi(t,r,i),!1}return!0}function fo(t,e,n){var r=n&&n.number,i=Ji(t,"value")||"null",o=Ji(t,"true-value")||"true",a=Ji(t,"false-value")||"false";Yi(t,"checked","Array.isArray("+e+")?_i("+e+","+i+")>-1"+("true"===o?":("+e+")":":_q("+e+","+o+")")),Xi(t,"change","var $$a="+e+",$$el=$event.target,$$c=$$el.checked?("+o+"):("+a+");if(Array.isArray($$a)){var $$v="+(r?"_n("+i+")":i)+",$$i=_i($$a,$$v);if($$el.checked){$$i<0&&("+to(e,"$$a.concat([$$v])")+")}else{$$i>-1&&("+to(e,"$$a.slice(0,$$i).concat($$a.slice($$i+1))")+")}}else{"+to(e,"$$c")+"}",null,!0)}function po(t,e,n){var r=n&&n.number,i=Ji(t,"value")||"null";i=r?"_n("+i+")":i,Yi(t,"checked","_q("+e+","+i+")"),Xi(t,"change",to(e,i),null,!0)}function ho(t,e,n){var r=n&&n.number,i='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")+"})",o="$event.target.multiple ? $$selectedVal : $$selectedVal[0]",a="var $$selectedVal = "+i+";";a=a+" "+to(e,o),Xi(t,"change",a,null,!0)}function vo(t,e,n){var r=t.attrsMap.type,i=n||{},o=i.lazy,a=i.number,s=i.trim,c=!o&&"range"!==r,u=o?"change":"range"===r?co:"input",l="$event.target.value";s&&(l="$event.target.value.trim()"),a&&(l="_n("+l+")");var f=to(e,l);c&&(f="if($event.target.composing)return;"+f),Yi(t,"value","("+e+")"),Xi(t,u,f,null,!0),(s||a)&&Xi(t,"blur","$forceUpdate()")}function mo(t){if(i(t[co])){var e=tt?"change":"input";t[e]=[].concat(t[co],t[e]||[]),delete t[co]}i(t[uo])&&(t.change=[].concat(t[uo],t.change||[]),delete t[uo])}function yo(t,e,n){var r=so;return function i(){var o=e.apply(null,arguments);null!==o&&bo(t,i,n,r)}}function go(t,e,n,r){e=fe(e),so.addEventListener(t,e,ot?{capture:n,passive:r}:n)}function bo(t,e,n,r){(r||so).removeEventListener(t,e._withTask||e,n)}function _o(t,e){if(!r(t.data.on)||!r(e.data.on)){var n=e.data.on||{},i=t.data.on||{};so=e.elm,mo(n),be(n,i,go,bo,yo,e.context),so=void 0}}var wo={create:_o,update:_o};function xo(t,e){if(!r(t.data.domProps)||!r(e.data.domProps)){var n,o,a=e.elm,s=t.data.domProps||{},c=e.data.domProps||{};for(n in i(c.__ob__)&&(c=e.data.domProps=T({},c)),s)r(c[n])&&(a[n]="");for(n in c){if(o=c[n],"textContent"===n||"innerHTML"===n){if(e.children&&(e.children.length=0),o===s[n])continue;1===a.childNodes.length&&a.removeChild(a.childNodes[0])}if("value"===n){a._value=o;var u=r(o)?"":String(o);$o(a,u)&&(a.value=u)}else a[n]=o}}}function $o(t,e){return!t.composing&&("OPTION"===t.tagName||Oo(t,e)||Co(t,e))}function Oo(t,e){var n=!0;try{n=document.activeElement!==t}catch(wu){}return n&&t.value!==e}function Co(t,e){var n=t.value,r=t._vModifiers;if(i(r)){if(r.lazy)return!1;if(r.number)return h(n)!==h(e);if(r.trim)return n.trim()!==e.trim()}return n!==e}var So={create:xo,update:xo},ko=w(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 Ao(t){var e=Eo(t.style);return t.staticStyle?T(t.staticStyle,e):e}function Eo(t){return Array.isArray(t)?j(t):"string"===typeof t?ko(t):t}function Mo(t,e){var n,r={};if(e){var i=t;while(i.componentInstance)i=i.componentInstance._vnode,i&&i.data&&(n=Ao(i.data))&&T(r,n)}(n=Ao(t.data))&&T(r,n);var o=t;while(o=o.parent)o.data&&(n=Ao(o.data))&&T(r,n);return r}var To,jo=/^--/,Po=/\s*!important$/,Do=function(t,e,n){if(jo.test(e))t.style.setProperty(e,n);else if(Po.test(n))t.style.setProperty(e,n.replace(Po,""),"important");else{var r=Io(e);if(Array.isArray(n))for(var i=0,o=n.length;i-1?e.split(Fo).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 zo(t,e){if(e&&(e=e.trim()))if(t.classList)e.indexOf(" ")>-1?e.split(Fo).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 Ho(t){if(t){if("object"===typeof t){var e={};return!1!==t.css&&T(e,Bo(t.name||"v")),T(e,t),e}return"string"===typeof t?Bo(t):void 0}}var Bo=w(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"}}),Vo=X&&!et,qo="transition",Yo="animation",Ko="transition",Wo="transitionend",Go="animation",Xo="animationend";Vo&&(void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend&&(Ko="WebkitTransition",Wo="webkitTransitionEnd"),void 0===window.onanimationend&&void 0!==window.onwebkitanimationend&&(Go="WebkitAnimation",Xo="webkitAnimationEnd"));var Jo=X?window.requestAnimationFrame?window.requestAnimationFrame.bind(window):setTimeout:function(t){return t()};function Zo(t){Jo(function(){Jo(t)})}function Qo(t,e){var n=t._transitionClasses||(t._transitionClasses=[]);n.indexOf(e)<0&&(n.push(e),Uo(t,e))}function ta(t,e){t._transitionClasses&&g(t._transitionClasses,e),zo(t,e)}function ea(t,e,n){var r=ra(t,e),i=r.type,o=r.timeout,a=r.propCount;if(!i)return n();var s=i===qo?Wo:Xo,c=0,u=function(){t.removeEventListener(s,l),n()},l=function(e){e.target===t&&++c>=a&&u()};setTimeout(function(){c0&&(n=qo,l=a,f=o.length):e===Yo?u>0&&(n=Yo,l=u,f=c.length):(l=Math.max(a,u),n=l>0?a>u?qo:Yo:null,f=n?n===qo?o.length:c.length:0);var p=n===qo&&na.test(r[Ko+"Property"]);return{type:n,timeout:l,propCount:f,hasTransform:p}}function ia(t,e){while(t.length1}function la(t,e){!0!==e.data.show&&aa(e)}var fa=X?{create:la,activate:la,remove:function(t,e){!0!==t.data.show?sa(t,e):e()}}:{},pa=[ji,Ui,wo,So,Ro,fa],da=pa.concat(Ai),ha=_i({nodeOps:pi,modules:da});et&&document.addEventListener("selectionchange",function(){var t=document.activeElement;t&&t.vmodel&&xa(t,"input")});var va={inserted:function(t,e,n,r){"select"===n.tag?(r.elm&&!r.elm._vOptions?_e(n,"postpatch",function(){va.componentUpdated(t,e,n)}):ma(t,e,n.context),t._vOptions=[].map.call(t.options,ba)):("textarea"===n.tag||Zr(t.type))&&(t._vModifiers=e.modifiers,e.modifiers.lazy||(t.addEventListener("compositionstart",_a),t.addEventListener("compositionend",wa),t.addEventListener("change",wa),et&&(t.vmodel=!0)))},componentUpdated:function(t,e,n){if("select"===n.tag){ma(t,e,n.context);var r=t._vOptions,i=t._vOptions=[].map.call(t.options,ba);if(i.some(function(t,e){return!N(t,r[e])})){var o=t.multiple?e.value.some(function(t){return ga(t,i)}):e.value!==e.oldValue&&ga(e.value,i);o&&xa(t,"change")}}}};function ma(t,e,n){ya(t,e,n),(tt||nt)&&setTimeout(function(){ya(t,e,n)},0)}function ya(t,e,n){var r=e.value,i=t.multiple;if(!i||Array.isArray(r)){for(var o,a,s=0,c=t.options.length;s-1,a.selected!==o&&(a.selected=o);else if(N(ba(a),r))return void(t.selectedIndex!==s&&(t.selectedIndex=s));i||(t.selectedIndex=-1)}}function ga(t,e){return e.every(function(e){return!N(e,t)})}function ba(t){return"_value"in t?t._value:t.value}function _a(t){t.target.composing=!0}function wa(t){t.target.composing&&(t.target.composing=!1,xa(t.target,"input"))}function xa(t,e){var n=document.createEvent("HTMLEvents");n.initEvent(e,!0,!0),t.dispatchEvent(n)}function $a(t){return!t.componentInstance||t.data&&t.data.transition?t:$a(t.componentInstance._vnode)}var Oa={bind:function(t,e,n){var r=e.value;n=$a(n);var i=n.data&&n.data.transition,o=t.__vOriginalDisplay="none"===t.style.display?"":t.style.display;r&&i?(n.data.show=!0,aa(n,function(){t.style.display=o})):t.style.display=r?o:"none"},update:function(t,e,n){var r=e.value,i=e.oldValue;if(!r!==!i){n=$a(n);var o=n.data&&n.data.transition;o?(n.data.show=!0,r?aa(n,function(){t.style.display=t.__vOriginalDisplay}):sa(n,function(){t.style.display="none"})):t.style.display=r?t.__vOriginalDisplay:"none"}},unbind:function(t,e,n,r,i){i||(t.style.display=t.__vOriginalDisplay)}},Ca={model:va,show:Oa},Sa={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 ka(t){var e=t&&t.componentOptions;return e&&e.Ctor.options.abstract?ka(Te(e.children)):t}function Aa(t){var e={},n=t.$options;for(var r in n.propsData)e[r]=t[r];var i=n._parentListeners;for(var o in i)e[$(o)]=i[o];return e}function Ea(t,e){if(/\d-keep-alive$/.test(e.tag))return t("keep-alive",{props:e.componentOptions.propsData})}function Ma(t){while(t=t.parent)if(t.data.transition)return!0}function Ta(t,e){return e.key===t.key&&e.tag===t.tag}var ja=function(t){return t.tag||Me(t)},Pa=function(t){return"show"===t.name},Da={name:"transition",props:Sa,abstract:!0,render:function(t){var e=this,n=this.$slots.default;if(n&&(n=n.filter(ja),n.length)){0;var r=this.mode;0;var i=n[0];if(Ma(this.$vnode))return i;var o=ka(i);if(!o)return i;if(this._leaving)return Ea(t,i);var a="__transition-"+this._uid+"-";o.key=null==o.key?o.isComment?a+"comment":a+o.tag:s(o.key)?0===String(o.key).indexOf(a)?o.key:a+o.key:o.key;var c=(o.data||(o.data={})).transition=Aa(this),u=this._vnode,l=ka(u);if(o.data.directives&&o.data.directives.some(Pa)&&(o.data.show=!0),l&&l.data&&!Ta(o,l)&&!Me(l)&&(!l.componentInstance||!l.componentInstance._vnode.isComment)){var f=l.data.transition=T({},c);if("out-in"===r)return this._leaving=!0,_e(f,"afterLeave",function(){e._leaving=!1,e.$forceUpdate()}),Ea(t,i);if("in-out"===r){if(Me(o))return u;var p,d=function(){p()};_e(c,"afterEnter",d),_e(c,"enterCancelled",d),_e(f,"delayLeave",function(t){p=t})}}return i}}},La=T({tag:String,moveClass:String},Sa);delete La.mode;var Ia={props:La,beforeMount:function(){var t=this,e=this._update;this._update=function(n,r){var i=He(t);t.__patch__(t._vnode,t.kept,!1,!0),t._vnode=t.kept,i(),e.call(t,n,r)}},render:function(t){for(var e=this.tag||this.$vnode.data.tag||"span",n=Object.create(null),r=this.prevChildren=this.children,i=this.$slots.default||[],o=this.children=[],a=Aa(this),s=0;sc&&(s.push(o=t.slice(c,i)),a.push(JSON.stringify(o)));var u=Hi(r[1].trim());a.push("_s("+u+")"),s.push({"@binding":u}),c=i+r[0].length}return c\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,rs="[a-zA-Z_][\\w\\-\\.]*",is="((?:"+rs+"\\:)?"+rs+")",os=new RegExp("^<"+is),as=/^\s*(\/?)>/,ss=new RegExp("^<\\/"+is+"[^>]*>"),cs=/^]+>/i,us=/^",""":'"',"&":"&"," ":"\n"," ":"\t"},hs=/&(?:lt|gt|quot|amp);/g,vs=/&(?:lt|gt|quot|amp|#10|#9);/g,ms=v("pre,textarea",!0),ys=function(t,e){return t&&ms(t)&&"\n"===e[0]};function gs(t,e){var n=e?vs:hs;return t.replace(n,function(t){return ds[t]})}function bs(t,e){var n,r,i=[],o=e.expectHTML,a=e.isUnaryTag||D,s=e.canBeLeftOpenTag||D,c=0;while(t){if(n=t,r&&fs(r)){var u=0,l=r.toLowerCase(),f=ps[l]||(ps[l]=new RegExp("([\\s\\S]*?)(]*>)","i")),p=t.replace(f,function(t,n,r){return u=r.length,fs(l)||"noscript"===l||(n=n.replace(//g,"$1").replace(//g,"$1")),ys(l,n)&&(n=n.slice(1)),e.chars&&e.chars(n),""});c+=t.length-p.length,t=p,S(l,c-u,c)}else{var d=t.indexOf("<");if(0===d){if(us.test(t)){var h=t.indexOf("--\x3e");if(h>=0){e.shouldKeepComment&&e.comment(t.substring(4,h)),$(h+3);continue}}if(ls.test(t)){var v=t.indexOf("]>");if(v>=0){$(v+2);continue}}var m=t.match(cs);if(m){$(m[0].length);continue}var y=t.match(ss);if(y){var g=c;$(y[0].length),S(y[1],g,c);continue}var b=O();if(b){C(b),ys(b.tagName,t)&&$(1);continue}}var _=void 0,w=void 0,x=void 0;if(d>=0){w=t.slice(d);while(!ss.test(w)&&!os.test(w)&&!us.test(w)&&!ls.test(w)){if(x=w.indexOf("<",1),x<0)break;d+=x,w=t.slice(d)}_=t.substring(0,d),$(d)}d<0&&(_=t,t=""),e.chars&&_&&e.chars(_)}if(t===n){e.chars&&e.chars(t);break}}function $(e){c+=e,t=t.substring(e)}function O(){var e=t.match(os);if(e){var n,r,i={tagName:e[1],attrs:[],start:c};$(e[0].length);while(!(n=t.match(as))&&(r=t.match(ns)))$(r[0].length),i.attrs.push(r);if(n)return i.unarySlash=n[1],$(n[0].length),i.end=c,i}}function C(t){var n=t.tagName,c=t.unarySlash;o&&("p"===r&&es(n)&&S(r),s(n)&&r===n&&S(n));for(var u=a(n)||!!c,l=t.attrs.length,f=new Array(l),p=0;p=0;a--)if(i[a].lowerCasedTag===s)break}else a=0;if(a>=0){for(var u=i.length-1;u>=a;u--)e.end&&e.end(i[u].tag,n,o);i.length=a,r=a&&i[a-1].tag}else"br"===s?e.start&&e.start(t,[],!0,n,o):"p"===s&&(e.start&&e.start(t,[],!1,n,o),e.end&&e.end(t,n,o))}S()}var _s,ws,xs,$s,Os,Cs,Ss,ks,As=/^@|^v-on:/,Es=/^v-|^@|^:/,Ms=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,Ts=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,js=/^\(|\)$/g,Ps=/:(.*)$/,Ds=/^:|^v-bind:/,Ls=/\.[^.]+/g,Is=w(Za.decode);function Ns(t,e,n){return{type:1,tag:t,attrsList:e,attrsMap:nc(e),parent:n,children:[]}}function Rs(t,e){_s=e.warn||Vi,Cs=e.isPreTag||D,Ss=e.mustUseProp||D,ks=e.getTagNamespace||D,xs=qi(e.modules,"transformNode"),$s=qi(e.modules,"preTransformNode"),Os=qi(e.modules,"postTransformNode"),ws=e.delimiters;var n,r,i=[],o=!1!==e.preserveWhitespace,a=!1,s=!1;function c(t){t.pre&&(a=!1),Cs(t.tag)&&(s=!1);for(var n=0;n|^function\s*\(/,Sc=/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/,kc={esc:27,tab:9,enter:13,space:32,up:38,left:37,right:39,down:40,delete:[8,46]},Ac={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"]},Ec=function(t){return"if("+t+")return null;"},Mc={stop:"$event.stopPropagation();",prevent:"$event.preventDefault();",self:Ec("$event.target !== $event.currentTarget"),ctrl:Ec("!$event.ctrlKey"),shift:Ec("!$event.shiftKey"),alt:Ec("!$event.altKey"),meta:Ec("!$event.metaKey"),left:Ec("'button' in $event && $event.button !== 0"),middle:Ec("'button' in $event && $event.button !== 1"),right:Ec("'button' in $event && $event.button !== 2")};function Tc(t,e){var n=e?"nativeOn:{":"on:{";for(var r in t)n+='"'+r+'":'+jc(r,t[r])+",";return n.slice(0,-1)+"}"}function jc(t,e){if(!e)return"function(){}";if(Array.isArray(e))return"["+e.map(function(e){return jc(t,e)}).join(",")+"]";var n=Sc.test(e.value),r=Cc.test(e.value);if(e.modifiers){var i="",o="",a=[];for(var s in e.modifiers)if(Mc[s])o+=Mc[s],kc[s]&&a.push(s);else if("exact"===s){var c=e.modifiers;o+=Ec(["ctrl","shift","alt","meta"].filter(function(t){return!c[t]}).map(function(t){return"$event."+t+"Key"}).join("||"))}else a.push(s);a.length&&(i+=Pc(a)),o&&(i+=o);var u=n?"return "+e.value+"($event)":r?"return ("+e.value+")($event)":e.value;return"function($event){"+i+u+"}"}return n||r?e.value:"function($event){"+e.value+"}"}function Pc(t){return"if(!('button' in $event)&&"+t.map(Dc).join("&&")+")return null;"}function Dc(t){var e=parseInt(t,10);if(e)return"$event.keyCode!=="+e;var n=kc[t],r=Ac[t];return"_k($event.keyCode,"+JSON.stringify(t)+","+JSON.stringify(n)+",$event.key,"+JSON.stringify(r)+")"}function Lc(t,e){t.wrapListeners=function(t){return"_g("+t+","+e.value+")"}}function Ic(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 Nc={on:Lc,bind:Ic,cloak:P},Rc=function(t){this.options=t,this.warn=t.warn||Vi,this.transforms=qi(t.modules,"transformCode"),this.dataGenFns=qi(t.modules,"genData"),this.directives=T(T({},Nc),t.directives);var e=t.isReservedTag||D;this.maybeComponent=function(t){return!(e(t.tag)&&!t.component)},this.onceId=0,this.staticRenderFns=[],this.pre=!1};function Fc(t,e){var n=new Rc(e),r=t?Uc(t,n):'_c("div")';return{render:"with(this){return "+r+"}",staticRenderFns:n.staticRenderFns}}function Uc(t,e){if(t.parent&&(t.pre=t.pre||t.parent.pre),t.staticRoot&&!t.staticProcessed)return zc(t,e);if(t.once&&!t.onceProcessed)return Hc(t,e);if(t.for&&!t.forProcessed)return qc(t,e);if(t.if&&!t.ifProcessed)return Bc(t,e);if("template"!==t.tag||t.slotTarget||e.pre){if("slot"===t.tag)return iu(t,e);var n;if(t.component)n=ou(t.component,t,e);else{var r;(!t.plain||t.pre&&e.maybeComponent(t))&&(r=Yc(t,e));var i=t.inlineTemplate?null:Zc(t,e,!0);n="_c('"+t.tag+"'"+(r?","+r:"")+(i?","+i:"")+")"}for(var o=0;o':'
',fu.innerHTML.indexOf(" ")>0}var mu=!!X&&vu(!1),yu=!!X&&vu(!0),gu=w(function(t){var e=Qr(t);return e&&e.innerHTML}),bu=dr.prototype.$mount;function _u(t){if(t.outerHTML)return t.outerHTML;var e=document.createElement("div");return e.appendChild(t.cloneNode(!0)),e.innerHTML}dr.prototype.$mount=function(t,e){if(t=t&&Qr(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=_u(t));if(r){0;var i=hu(r,{shouldDecodeNewlines:mu,shouldDecodeNewlinesForHref:yu,delimiters:n.delimiters,comments:n.comments},this),o=i.render,a=i.staticRenderFns;n.render=o,n.staticRenderFns=a}}return bu.call(this,t,e)},dr.compile=hu,e["a"]=dr}).call(this,n("c8ba"))},a25f:function(t,e,n){var r=n("7726"),i=r.navigator;t.exports=i&&i.userAgent||""},a481:function(t,e,n){"use strict";var r=n("cb7c"),i=n("4bf8"),o=n("9def"),a=n("4588"),s=n("0390"),c=n("5f1b"),u=Math.max,l=Math.min,f=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,i){var o=t(this),a=void 0==r?void 0:r[e];return void 0!==a?a.call(r,o,i):n.call(String(o),r,i)},function(t,e){var i=v(n,t,this,e);if(i.done)return i.value;var f=r(t),p=String(this),d="function"===typeof e;d||(e=String(e));var y=f.global;if(y){var g=f.unicode;f.lastIndex=0}var b=[];while(1){var _=c(f,p);if(null===_)break;if(b.push(_),!y)break;var w=String(_[0]);""===w&&(f.lastIndex=s(p,o(f.lastIndex),g))}for(var x="",$=0,O=0;O=$&&(x+=p.slice($,S)+T,$=S+C.length)}return x+p.slice($)}];function m(t,e,r,o,a,s){var c=r+t.length,u=o.length,l=d;return void 0!==a&&(a=i(a),l=p),n.call(s,l,function(n,i){var s;switch(i.charAt(0)){case"$":return"$";case"&":return t;case"`":return e.slice(0,r);case"'":return e.slice(c);case"<":s=a[i.slice(1,-1)];break;default:var l=+i;if(0===l)return i;if(l>u){var p=f(l/10);return 0===p?i:p<=u?void 0===o[p-1]?i.charAt(1):o[p-1]+i.charAt(1):i}s=o[l-1]}return void 0===s?"":s})}})},a5b8:function(t,e,n){"use strict";var r=n("d8e8");function i(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 i(t)}},aa77:function(t,e,n){var r=n("5ca1"),i=n("be13"),o=n("79e5"),a=n("fdef"),s="["+a+"]",c="​…",u=RegExp("^"+s+s+"*"),l=RegExp(s+s+"*$"),f=function(t,e,n){var i={},s=o(function(){return!!a[t]()||c[t]()!=c}),u=i[t]=s?e(p):a[t];n&&(i[n]=u),r(r.P+r.F*s,"String",i)},p=f.trim=function(t,e){return t=String(i(t)),1&e&&(t=t.replace(u,"")),2&e&&(t=t.replace(l,"")),t};t.exports=f},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)})}},aae3:function(t,e,n){var r=n("d3f4"),i=n("2d95"),o=n("2b4c")("match");t.exports=function(t){var e;return r(t)&&(void 0!==(e=t[o])?!!e:"RegExp"==i(t))}},ac6a:function(t,e,n){for(var r=n("cadf"),i=n("0d58"),o=n("2aba"),a=n("7726"),s=n("32e9"),c=n("84f2"),u=n("2b4c"),l=u("iterator"),f=u("toStringTag"),p=c.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=i(d),v=0;v":"greater","|":"or","¢":"cent","£":"pound","¤":"currency","¥":"yen","©":"(c)","ª":"a","®":"(r)","º":"o","À":"A","Á":"A","Â":"A","Ã":"A","Ä":"A","Å":"A","Æ":"AE","Ç":"C","È":"E","É":"E","Ê":"E","Ë":"E","Ì":"I","Í":"I","Î":"I","Ï":"I","Ð":"D","Ñ":"N","Ò":"O","Ó":"O","Ô":"O","Õ":"O","Ö":"O","Ø":"O","Ù":"U","Ú":"U","Û":"U","Ü":"U","Ý":"Y","Þ":"TH","ß":"ss","à":"a","á":"a","â":"a","ã":"a","ä":"a","å":"a","æ":"ae","ç":"c","è":"e","é":"e","ê":"e","ë":"e","ì":"i","í":"i","î":"i","ï":"i","ð":"d","ñ":"n","ò":"o","ó":"o","ô":"o","õ":"o","ö":"o","ø":"o","ù":"u","ú":"u","û":"u","ü":"u","ý":"y","þ":"th","ÿ":"y","Ā":"A","ā":"a","Ă":"A","ă":"a","Ą":"A","ą":"a","Ć":"C","ć":"c","Č":"C","č":"c","Ď":"D","ď":"d","Đ":"DJ","đ":"dj","Ē":"E","ē":"e","Ė":"E","ė":"e","Ę":"e","ę":"e","Ě":"E","ě":"e","Ğ":"G","ğ":"g","Ģ":"G","ģ":"g","Ĩ":"I","ĩ":"i","Ī":"i","ī":"i","Į":"I","į":"i","İ":"I","ı":"i","Ķ":"k","ķ":"k","Ļ":"L","ļ":"l","Ľ":"L","ľ":"l","Ł":"L","ł":"l","Ń":"N","ń":"n","Ņ":"N","ņ":"n","Ň":"N","ň":"n","Ő":"O","ő":"o","Œ":"OE","œ":"oe","Ŕ":"R","ŕ":"r","Ř":"R","ř":"r","Ś":"S","ś":"s","Ş":"S","ş":"s","Š":"S","š":"s","Ţ":"T","ţ":"t","Ť":"T","ť":"t","Ũ":"U","ũ":"u","Ū":"u","ū":"u","Ů":"U","ů":"u","Ű":"U","ű":"u","Ų":"U","ų":"u","Ź":"Z","ź":"z","Ż":"Z","ż":"z","Ž":"Z","ž":"z","ƒ":"f","Ơ":"O","ơ":"o","Ư":"U","ư":"u","Lj":"LJ","lj":"lj","Nj":"NJ","nj":"nj","Ș":"S","ș":"s","Ț":"T","ț":"t","˚":"o","Ά":"A","Έ":"E","Ή":"H","Ί":"I","Ό":"O","Ύ":"Y","Ώ":"W","ΐ":"i","Α":"A","Β":"B","Γ":"G","Δ":"D","Ε":"E","Ζ":"Z","Η":"H","Θ":"8","Ι":"I","Κ":"K","Λ":"L","Μ":"M","Ν":"N","Ξ":"3","Ο":"O","Π":"P","Ρ":"R","Σ":"S","Τ":"T","Υ":"Y","Φ":"F","Χ":"X","Ψ":"PS","Ω":"W","Ϊ":"I","Ϋ":"Y","ά":"a","έ":"e","ή":"h","ί":"i","ΰ":"y","α":"a","β":"b","γ":"g","δ":"d","ε":"e","ζ":"z","η":"h","θ":"8","ι":"i","κ":"k","λ":"l","μ":"m","ν":"n","ξ":"3","ο":"o","π":"p","ρ":"r","ς":"s","σ":"s","τ":"t","υ":"y","φ":"f","χ":"x","ψ":"ps","ω":"w","ϊ":"i","ϋ":"y","ό":"o","ύ":"y","ώ":"w","Ё":"Yo","Ђ":"DJ","Є":"Ye","І":"I","Ї":"Yi","Ј":"J","Љ":"LJ","Њ":"NJ","Ћ":"C","Џ":"DZ","А":"A","Б":"B","В":"V","Г":"G","Д":"D","Е":"E","Ж":"Zh","З":"Z","И":"I","Й":"J","К":"K","Л":"L","М":"M","Н":"N","О":"O","П":"P","Р":"R","С":"S","Т":"T","У":"U","Ф":"F","Х":"H","Ц":"C","Ч":"Ch","Ш":"Sh","Щ":"Sh","Ъ":"U","Ы":"Y","Ь":"","Э":"E","Ю":"Yu","Я":"Ya","а":"a","б":"b","в":"v","г":"g","д":"d","е":"e","ж":"zh","з":"z","и":"i","й":"j","к":"k","л":"l","м":"m","н":"n","о":"o","п":"p","р":"r","с":"s","т":"t","у":"u","ф":"f","х":"h","ц":"c","ч":"ch","ш":"sh","щ":"sh","ъ":"u","ы":"y","ь":"","э":"e","ю":"yu","я":"ya","ё":"yo","ђ":"dj","є":"ye","і":"i","ї":"yi","ј":"j","љ":"lj","њ":"nj","ћ":"c","џ":"dz","Ґ":"G","ґ":"g","฿":"baht","ა":"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","ẞ":"SS","Ạ":"A","ạ":"a","Ả":"A","ả":"a","Ấ":"A","ấ":"a","Ầ":"A","ầ":"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","ể":"e","Ễ":"E","ễ":"e","Ệ":"E","ệ":"e","Ỉ":"I","ỉ":"i","Ị":"I","ị":"i","Ọ":"O","ọ":"o","Ỏ":"O","ỏ":"o","Ố":"O","ố":"o","Ồ":"O","ồ":"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","ữ":"u","Ự":"U","ự":"u","Ỳ":"Y","ỳ":"y","Ỵ":"Y","ỵ":"y","Ỷ":"Y","ỷ":"y","Ỹ":"Y","ỹ":"y","‘":"\'","’":"\'","“":"\\"","”":"\\"","†":"+","•":"*","…":"...","₠":"ecu","₢":"cruzeiro","₣":"french franc","₤":"lira","₥":"mill","₦":"naira","₧":"peseta","₨":"rupee","₩":"won","₪":"new shequel","₫":"dong","€":"euro","₭":"kip","₮":"tugrik","₯":"drachma","₰":"penny","₱":"peso","₲":"guarani","₳":"austral","₴":"hryvnia","₵":"cedi","₹":"indian rupee","₽":"russian ruble","₿":"bitcoin","℠":"sm","™":"tm","∂":"d","∆":"delta","∑":"sum","∞":"infinity","♥":"love","元":"yuan","円":"yen","﷼":"rial"}');function e(e,n){if("string"!==typeof e)throw new Error("slugify: string argument expected");n="string"===typeof n?{replacement:n}:n||{};var r=e.split("").reduce(function(e,r){return e+(t[r]||r).replace(n.remove||/[^\w\s$*_+~.()'"!\-:@]/g,"")},"").trim().replace(/[-\s]+/g,n.replacement||"-");return n.lower?r.toLowerCase():r}return e.extend=function(e){for(var n in e)t[n]=e[n]},e})},bcaa:function(t,e,n){var r=n("cb7c"),i=n("d3f4"),o=n("a5b8");t.exports=function(t,e){if(r(t),i(e)&&e.constructor===t)return e;var n=o.f(t),a=n.resolve;return a(e),n.promise}},be13:function(t,e){t.exports=function(t){if(void 0==t)throw TypeError("Can't call method on "+t);return t}},be94:function(t,e,n){"use strict";function r(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function i(t){for(var e=1;el)if(s=c[l++],s!=s)return!0}else for(;u>l;l++)if((t||l in c)&&c[l]===n)return t||l||0;return!t&&-1}}},c5f6:function(t,e,n){"use strict";var r=n("7726"),i=n("69a8"),o=n("2d95"),a=n("5dbc"),s=n("6a99"),c=n("79e5"),u=n("9093").f,l=n("11e9").f,f=n("86cc").f,p=n("aa77").trim,d="Number",h=r[d],v=h,m=h.prototype,y=o(n("2aeb")(m))==d,g="trim"in String.prototype,b=function(t){var e=s(t,!1);if("string"==typeof e&&e.length>2){e=g?e.trim():p(e,3);var n,r,i,o=e.charCodeAt(0);if(43===o||45===o){if(n=e.charCodeAt(2),88===n||120===n)return NaN}else if(48===o){switch(e.charCodeAt(1)){case 66:case 98:r=2,i=49;break;case 79:case 111:r=8,i=55;break;default:return+e}for(var a,c=e.slice(2),u=0,l=c.length;ui)return NaN;return parseInt(c,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&&(y?c(function(){m.valueOf.call(n)}):o(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++)i(v,_=w[x])&&!i(h,_)&&f(h,_,l(v,_));h.prototype=m,m.constructor=h,n("2aba")(r,d,h)}},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},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(i)});var i=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}},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"),i=n("d53b"),o=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,i(1)):i(0,"keys"==e?n:"values"==e?t[n]:[n,t[n]])},"values"),o.Arguments=o.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}},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"),i=n("6821"),o=n("c366")(!1),a=n("613b")("IE_PROTO");t.exports=function(t,e){var n,s=i(t),c=0,u=[];for(n in s)n!=a&&r(s,n)&&u.push(n);while(e.length>c)r(s,n=e[c++])&&(~o(u,n)||u.push(n));return u}},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"),i=n("be13");t.exports=function(t,e,n){if(r(e))throw TypeError("String#"+n+" doesn't accept regex!");return String(i(t))}},d3f4:function(t,e){t.exports=function(t){return"object"===typeof t?null!==t:"function"===typeof 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}}},d8e8:function(t,e){t.exports=function(t){if("function"!=typeof t)throw TypeError(t+" is not a function!");return t}},dcbc:function(t,e,n){var r=n("2aba");t.exports=function(t,e,n){for(var i in e)r(t,i,e[i],n);return t}},e11e:function(t,e){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},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)})}},e853:function(t,e,n){var r=n("d3f4"),i=n("1169"),o=n("2b4c")("species");t.exports=function(t){var e;return i(t)&&(e=t.constructor,"function"!=typeof e||e!==Array&&!i(e.prototype)||(e=void 0),r(e)&&(e=e[o],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"),i=n("d8e8"),o=n("2b4c")("species");t.exports=function(t,e){var n,a=r(t).constructor;return void 0===a||void 0==(n=r(a)[o])?e:i(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})}},f2f3:function(t,e,n){"use strict";var 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},i={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=o(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(i){}},REPLACE_LOCALE:function(t,e){var n=o(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})}}},o=function t(e){var n={};for(var i in e)if(e.hasOwnProperty(i)){var o=r(e[i]);if(a(e[i])){for(var s=e[i].length,c=0;c1?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}}},c={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]});var r=Object.assign({moduleName:"i18n",identifiers:["{","}"],preserveState:!1,onTranslationNotFound:function(){}},n),o=r.moduleName,a=r.identifiers,s=r.onTranslationNotFound;if("function"!==typeof s&&(console.error("i18n: i18n config option onTranslationNotFound must be a function"),s=function(){}),e.registerModule(o,i,{preserveState:r.preserveState}),!1===e.state.hasOwnProperty(o))return console.error("i18n: i18n vuex module is not correctly initialized. Please check the module name:",o),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 c=u(a),l=function(){var t=e.state[o].locale;return f.apply(void 0,[t].concat(Array.prototype.slice.call(arguments)))},f=function(t){var n=arguments,r="",i="",a={},u=null,l=n.length;if(l>=3&&"string"===typeof n[2]?(r=n[1],i=n[2],l>3&&(a=n[3]),l>4&&(u=n[4])):(r=n[1],i=r,l>2&&(a=n[2]),l>3&&(u=n[3])),!t)return console.warn("i18n: i18n locale is not set when trying to access translations:",r),i;var f=e.state[o].translations,p=e.state[o].fallback,d=t.split("-"),h=!0;if(!1===f.hasOwnProperty(t)?h=!1:!1===f[t].hasOwnProperty(r)&&(h=!1),!0===h)return c(t,f[t][r],a,u);if(d.length>1&&!0===f.hasOwnProperty(d[0])&&!0===f[d[0]].hasOwnProperty(r))return c(d[0],f[d[0]][r],a,u);var v=s(t,r,i);return v&&Promise.resolve(v).then(function(e){var n={};n[r]=e,y(t,n)}),!1===f.hasOwnProperty(p)?c(t,i,a,u):!1===f[p].hasOwnProperty(r)?c(p,i,a,u):c(t,f[p][r],a,u)},p=function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"fallback",r=e.state[o].locale,i=e.state[o].fallback,a=e.state[o].translations;if(a.hasOwnProperty(r)&&a[r].hasOwnProperty(t))return!0;if("strict"==n)return!1;var s=r.split("-");return!!(s.length>1&&a.hasOwnProperty(s[0])&&a[s[0]].hasOwnProperty(t))||"locale"!=n&&!(!a.hasOwnProperty(i)||!a[i].hasOwnProperty(t))},d=function(t){e.dispatch({type:o+"/setFallbackLocale",locale:t})},h=function(t){e.dispatch({type:o+"/setLocale",locale:t})},v=function(){return e.state[o].locale},m=function(){return Object.keys(e.state[o].translations)},y=function(t,n){return e.dispatch({type:o+"/addLocale",locale:t,translations:n})},g=function(t,n){return e.dispatch({type:o+"/replaceLocale",locale:t,translations:n})},b=function(t){e.state[o].translations.hasOwnProperty(t)&&e.dispatch({type:o+"/removeLocale",locale:t})},_=function(t){return console.warn("i18n: $i18n.exists is depreceated. Please use $i18n.localeExists instead. It provides exatly the same functionality."),w(t)},w=function(t){return e.state[o].translations.hasOwnProperty(t)};t.prototype.$i18n={locale:v,locales:m,set:h,add:y,replace:g,remove:b,fallback:d,localeExists:w,keyExists:p,translate:l,translateIn:f,exists:_},t.i18n={locale:v,locales:m,set:h,add:y,replace:g,remove:b,fallback:d,translate:l,translateIn:f,localeExists:w,keyExists:p,exists:_},t.prototype.$t=l,t.prototype.$tlang=f,t.filter("translate",l)}},u=function(t){null!=t&&2==t.length||console.warn("i18n: You must specify the start and end character identifying variable substitutions");var e=new RegExp(t[0]+"\\w+"+t[1],"g"),n=function(n,r){var i=!(arguments.length>2&&void 0!==arguments[2])||arguments[2];return n.replace?n.replace(e,function(e){var o=e.replace(t[0],"").replace(t[1],"");return void 0!==r[o]?r[o]:(!0===i&&(console.group?console.group("i18n: Not all placeholders found"):console.warn("i18n: Not all placeholders found"),console.warn("Text:",n),console.warn("Placeholder:",e),console.groupEnd&&console.groupEnd()),e)}):n},i=function(t,e){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null,a="undefined"===typeof e?"undefined":r(e),c="undefined"===typeof o?"undefined":r(o),u=function(){return l(e)?e.map(function(t){return n(t,i,!1)}):"string"===a?n(e,i,!0):void 0};if(null===o)return u();if("number"!==c)return console.warn("i18n: pluralization is not a number"),u();var f=u(),p=null;p=l(f)&&f.length>0?f:f.split(":::");var d=s.getTranslationIndex(t,o);return"undefined"===typeof p[d]?(console.warn("i18n: pluralization not provided in locale",e,t,d),p[0].trim()):p[d].trim()};return i};function l(t){return!!t&&Array===t.constructor}var f={store:i,plugin:c};e["a"]=f},f605: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}},f751:function(t,e,n){var r=n("5ca1");r(r.S+r.F,"Object",{assign:n("7333")})},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 i(t){return null!==t&&void 0!==t}function o(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 s(t){for(var e=Object.keys(t.args),n=0;nu?l(e,s,v):s>v&&f(t,n,u)}function l(t,e,n){for(;e<=n;++e)a(t[e])}function f(t,e,n){for(;e<=n;++e){var r=t[e];i(r)&&(r.vm.$destroy(),r.vm=null)}}function p(t,e){t!==e&&(e.vm=t.vm,s(e))}function d(t,e){i(t)&&i(e)?t!==e&&u(t,e):i(e)?l(e,0,e.length-1):i(t)&&f(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},fdef:function(t,e){t.exports="\t\n\v\f\r   ᠎              \u2028\u2029\ufeff"},ffc1:function(t,e,n){var r=n("5ca1"),i=n("504c")(!0);r(r.S,"Object",{entries:function(t){return i(t)}})}}]); \ No newline at end of file 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 @@ +data($method, ...$args); + } + + public function __construct(array $props) + { + $this->setProperties($props); + } + + public function authenticate() + { + if ($auth = $this->authentication()) { + return $auth->call($this); + } + + return true; + } + + public function authentication() + { + return $this->authentication; + } + + public function call(string $path = null, string $method = 'GET', array $requestData = []) + { + $path = rtrim($path, '/'); + + $this->setRequestMethod($method); + $this->setRequestData($requestData); + + $router = new Router($this->routes()); + $result = $router->find($path, $method); + $auth = $result->attributes()['auth'] ?? true; + + if ($auth !== false) { + $this->authenticate(); + } + + $output = $result->action()->call($this, ...$result->arguments()); + + if (is_object($output) === true) { + return $this->resolve($output)->toResponse(); + } + + return $output; + } + + 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]); + } + + public function collections(): array + { + return $this->collections; + } + + 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]; + } + + public function hasData($key): bool + { + return isset($this->data[$key]) === true; + } + + 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]); + } + + public function models(): array + { + return $this->models; + } + + public function requestData($type = null, $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; + } + + public function requestBody(string $key = null, $default = null) + { + return $this->requestData('body', $key, $default); + } + + public function requestFiles(string $key = null, $default = null) + { + return $this->requestData('files', $key, $default); + } + + public function requestHeaders(string $key = null, $default = null) + { + return $this->requestData('headers', $key, $default); + } + + public function requestMethod(): string + { + return $this->requestMethod; + } + + public function requestQuery(string $key = null, $default = null) + { + return $this->requestData('query', $key, $default); + } + + 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)); + } + + public function routes(): array + { + return $this->routes; + } + + protected function setAuthentication(Closure $authentication = null) + { + $this->authentication = $authentication; + return $this; + } + + protected function setCollections(array $collections = null) + { + if ($collections !== null) { + $this->collections = array_change_key_case($collections); + } + return $this; + } + + protected function setData(array $data = null) + { + $this->data = $data ?? []; + return $this; + } + + protected function setDebug(bool $debug = false) + { + $this->debug = $debug; + return $this; + } + + protected function setModels(array $models = null) + { + if ($models !== null) { + $this->models = array_change_key_case($models); + } + + return $this; + } + + protected function setRequestData(array $requestData = null) + { + $defaults = [ + 'query' => [], + 'body' => [], + 'files' => [] + ]; + + $this->requestData = array_merge($defaults, (array)$requestData); + return $this; + } + + protected function setRequestMethod(string $requestMethod = null) + { + $this->requestMethod = $requestMethod; + return $this; + } + + protected function setRoutes(array $routes = null) + { + $this->routes = $routes ?? []; + return $this; + } + + public function render(string $path, $method = 'GET', array $requestData = []) + { + try { + $result = $this->call($path, $method, $requestData); + } catch (Throwable $e) { + if (is_a($e, 'Kirby\Exception\Exception') === true) { + $result = ['status' => 'error'] + $e->toArray(); + } else { + $result = [ + 'status' => 'error', + 'exception' => get_class($e), + 'message' => $e->getMessage(), + 'file' => ltrim($e->getFile(), $_SERVER['DOCUMENT_ROOT'] ?? null), + 'line' => $e->getLine(), + 'code' => empty($e->getCode()) === false ? $e->getCode() : 500 + ]; + } + } + + if ($result === null) { + $result = [ + 'status' => 'error', + 'message' => 'not found', + 'code' => 404, + ]; + } + + if ($result === true) { + $result = [ + 'status' => 'ok', + ]; + } + + if ($result === false) { + $result = [ + 'status' => 'error', + 'message' => 'bad request', + 'code' => 400, + ]; + } + + if (is_array($result) === false) { + return $result; + } + + // pretty print json data + $pretty = (bool)($requestData['query']['pretty'] ?? false) === true; + + // remove critical info from the result set if + // debug mode is switched off + if ($this->debug !== true) { + unset( + $result['file'], + $result['exception'], + $result['line'] + ); + } + + 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); + } + + public function upload(Closure $callback, $single = false): array + { + $trials = 0; + $uploads = []; + $errors = []; + $files = $this->requestFiles(); + + if (empty($files) === true) { + throw new Exception('No uploaded files'); + } + + foreach ($files as $upload) { + if (isset($upload['tmp_name']) === false && is_array($upload)) { + continue; + } + + $trials++; + + try { + if ($upload['error'] !== 0) { + throw new Exception('Upload error'); + } + + // 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('The uploaded file could not be moved'); + } + + $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..8ab61dd --- /dev/null +++ b/kirby/src/Api/Collection.php @@ -0,0 +1,124 @@ +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..17a9bf9 --- /dev/null +++ b/kirby/src/Api/Model.php @@ -0,0 +1,188 @@ +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..d81926b --- /dev/null +++ b/kirby/src/Cache/ApcuCache.php @@ -0,0 +1,77 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license MIT + */ +class ApcuCache extends Cache +{ + + /** + * Write an item to the cache for a given number of minutes. + * + * + * // 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 void + */ + public function set(string $key, $value, int $minutes = 0) + { + return apcu_store($key, $this->value($value, $minutes)->toJson(), $this->expiration($minutes)); + } + + /** + * Retrieve an item from the cache. + * + * @param string $key + * @return mixed + */ + public function retrieve(string $key) + { + return Value::fromJson(apcu_fetch($key)); + } + + /** + * Checks if the current key exists in cache + * + * @param string $key + * @return boolean + */ + public function exists(string $key): bool + { + return apcu_exists($key); + } + + /** + * Remove an item from the cache + * + * @param string $key + * @return boolean + */ + public function remove(string $key): bool + { + return apcu_delete($key); + } + + /** + * Flush the entire cache directory + * + * @return boolean + */ + public function flush(): bool + { + return apcu_clear_cache(); + } +} diff --git a/kirby/src/Cache/Cache.php b/kirby/src/Cache/Cache.php new file mode 100755 index 0000000..eb4472b --- /dev/null +++ b/kirby/src/Cache/Cache.php @@ -0,0 +1,237 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license MIT + */ +class Cache +{ + + /** + * stores all options for the driver + * @var array + */ + protected $options = []; + + /** + * Set all parameters which are needed to connect to the cache storage + * + * @param array $options + */ + public function __construct(array $options = []) + { + $this->options = $options; + } + + /** + * Write an item to the cache for a given number of minutes. + * + * + * // 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 void + */ + public function set(string $key, $value, int $minutes = 0) + { + return null; + } + + /** + * Private method to retrieve the cache value + * This needs to be defined by the driver + * + * @param string $key + * @return mixed + */ + public function retrieve(string $key) + { + return null; + } + + /** + * Get 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 (time() >= $value->expires()) { + $this->remove($key); + return $default; + } + + // get the pure value + $cache = $value->value(); + + // return the cache value or the default + return $cache ?? $default; + } + + /** + * Calculates the expiration timestamp + * + * @param int $minutes + * @return int + */ + protected function expiration(int $minutes = 0): int + { + // keep forever if minutes are not defined + if ($minutes === 0) { + $minutes = 2628000; + } + + // calculate the time + return time() + ($minutes * 60); + } + + /** + * Checks when an item in the cache expires + * + * @param string $key + * @return mixed + */ + 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 boolean + */ + public function expired(string $key): bool + { + return $this->expires($key) <= time(); + } + + /** + * Checks when the cache has been created + * + * @param string $key + * @return mixed + */ + 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 mixed + */ + public function modified(string $key) + { + return static::created($key); + } + + /** + * Returns Value object + * + * @param mixed $value The value, which should be cached + * @param int $minutes The number of minutes before expiration + * @return Value + */ + protected function value($value, int $minutes): Value + { + return new Value($value, $minutes); + } + + /** + * Determine if an item exists in the cache. + * + * @param string $key + * @return boolean + */ + public function exists(string $key): bool + { + return !$this->expired($key); + } + + /** + * Remove an item from the cache + * + * @param string $key + * @return boolean + */ + public function remove(string $key): bool + { + return true; + } + + /** + * Flush the entire cache + * + * @return boolean + */ + public function flush(): bool + { + return true; + } + + /** + * 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..f989a45 --- /dev/null +++ b/kirby/src/Cache/FileCache.php @@ -0,0 +1,126 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license MIT + */ +class FileCache extends Cache +{ + + /** + * Set all parameters which are needed for the file cache + * see defaults for available parameters + * + * @param array $params + */ + public function __construct(array $params) + { + $defaults = [ + 'root' => null, + 'extension' => null + ]; + + parent::__construct(array_merge($defaults, $params)); + + // try to create the directory + Dir::make($this->options['root'], true); + + // check for a valid cache directory + if (is_dir($this->options['root']) === false) { + throw new Exception('The cache directory does not exist'); + } + } + + /** + * Returns the full path to a file for a given key + * + * @param string $key + * @return string + */ + protected function file(string $key): string + { + $extension = isset($this->options['extension']) ? '.' . $this->options['extension'] : ''; + return $this->options['root'] . '/' . $key . $extension; + } + + /** + * Write an item to the cache for a given number of minutes. + * + * + * // Put an item in the cache for 15 minutes + * Cache::set('value', 'my value', 15); + * + * + * @param string $key + * @param mixed $value + * @param int $minutes + */ + public function set(string $key, $value, int $minutes = 0) + { + return F::write($this->file($key), $this->value($value, $minutes)->toJson()); + } + + /** + * Retrieve an item from the cache. + * + * @param string $key + * @return mixed + */ + public function retrieve(string $key) + { + return Value::fromJson(F::read($this->file($key))); + } + + /** + * Checks when the cache has been created + * + * @param string $key + * @return int + */ + public function created(string $key): int + { + // 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)) : 0; + } + + /** + * Remove an item from the cache + * + * @param string $key + * @return boolean + */ + public function remove(string $key): bool + { + return F::remove($this->file($key)); + } + + /** + * Flush the entire cache directory + * + * @return boolean + */ + public function flush(): bool + { + if (Dir::remove($this->options['root']) === true && Dir::make($this->options['root']) === true) { + return true; + } + + return false; + } +} diff --git a/kirby/src/Cache/MemCached.php b/kirby/src/Cache/MemCached.php new file mode 100755 index 0000000..356e22d --- /dev/null +++ b/kirby/src/Cache/MemCached.php @@ -0,0 +1,137 @@ + +* @link http://getkirby.com +* @copyright Bastian Allgeier +* @license MIT +*/ +class MemCached extends Cache +{ + + /** + * store for the memache connection + * @var Memcached + */ + protected $connection; + + /** + * Set all parameters which are needed for the memcache client + * see defaults for available parameters + * + * @param array $params + */ + public function __construct(array $params = []) + { + $defaults = [ + 'host' => 'localhost', + 'port' => 11211, + 'prefix' => null, + ]; + + parent::__construct(array_merge($defaults, $params)); + + $this->connection = new \Memcached(); + $this->connection->addServer($this->options['host'], $this->options['port']); + } + + /** + * Write an item to the cache for a given number of minutes. + * + * + * // 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 void + */ + public function set(string $key, $value, int $minutes = 0) + { + return $this->connection->set($this->key($key), $this->value($value, $minutes)->toJson(), $this->expiration($minutes)); + } + + /** + * Returns the full keyname + * including the prefix (if set) + * + * @param string $key + * @return string + */ + public function key(string $key): string + { + return $this->options['prefix'] . $key; + } + + /** + * Retrieve the CacheValue object from the cache. + * + * @param string $key + * @return object CacheValue + */ + public function retrieve(string $key) + { + return Value::fromJson($this->connection->get($this->key($key))); + } + + /** + * Remove an item from the cache + * + * @param string $key + * @return boolean + */ + public function remove(string $key): bool + { + return $this->connection->delete($this->key($key)); + } + + /** + * Checks when an item in the cache expires + * + * @param string $key + * @return int + */ + public function expires(string $key): int + { + return parent::expires($this->key($key)); + } + + /** + * Checks if an item in the cache is expired + * + * @param string $key + * @return boolean + */ + public function expired(string $key): bool + { + return parent::expired($this->key($key)); + } + + /** + * Checks when the cache has been created + * + * @param string $key + * @return int + */ + public function created(string $key): int + { + return parent::created($this->key($key)); + } + + /** + * Flush the entire cache directory + * + * @return boolean + */ + public function flush(): bool + { + return $this->connection->flush(); + } +} diff --git a/kirby/src/Cache/Value.php b/kirby/src/Cache/Value.php new file mode 100755 index 0000000..755fbdf --- /dev/null +++ b/kirby/src/Cache/Value.php @@ -0,0 +1,139 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license MIT + */ +class Value +{ + + /** + * the cached value + * @var mixed + */ + protected $value; + + /** + * the expiration timestamp + * @var int + */ + protected $expires; + + /** + * the 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, $created = null) + { + // keep forever if minutes are not defined + if ($minutes === 0) { + $minutes = 2628000; + } + + $this->value = $value; + $this->minutes = $minutes; + $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 + * + * @return int + */ + public function expires(): int + { + return $this->created + ($this->minutes * 60); + } + + /** + * Creates a value object from an array + * + * @param array $array + * @return array + */ + public static function fromArray(array $array): self + { + return new static($array['value'] ?? null, $array['minutes'] ?? 0, $array['created'] ?? null); + } + + /** + * Creates a value object from a json string + * + * @param string $json + * @return array + */ + public static function fromJson($json): self + { + try { + $array = json_decode($json, true) ?? []; + } catch (Throwable $e) { + $array = []; + } + + return static::fromArray($array); + } + + /** + * Convert the object to a json string + * + * @return string + */ + public function toJson(): string + { + return json_encode($this->toArray()); + } + + /** + * Convert the object to an array + * + * @return array + */ + public function toArray(): array + { + return [ + 'created' => $this->created, + 'minutes' => $this->minutes, + 'value' => $this->value, + ]; + } + + /** + * Returns the 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..8b4d4ec --- /dev/null +++ b/kirby/src/Cms/Api.php @@ -0,0 +1,176 @@ +setRequestMethod($method); + $this->setRequestData($requestData); + + if ($languageCode = $this->requestHeaders('x-language')) { + $this->kirby->setCurrentLanguage($languageCode); + } + + if ($user = $this->kirby->user()) { + $this->kirby->setCurrentTranslation($user->language()); + } + + return parent::call($path, $method, $requestData); + } + + 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()); + } + + 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 + ] + ]); + } + + public function parent(string $path) + { + $modelType = $path === 'site' ? 'site' : dirname($path); + $modelTypes = ['site' => 'site', 'users' => 'user', 'pages' => 'page']; + $modelName = $modelTypes[$modelType] ?? null; + + if ($modelName === null) { + throw new InvalidArgumentException('Invalid file model type'); + } + + if ($modelName === 'site') { + $modelId = null; + } else { + $modelId = basename($path); + + if ($modelName === 'page') { + $modelId = str_replace('+', '/', $modelId); + } + } + + if ($model = $this->kirby()->$modelName($modelId)) { + return $model; + } + + throw new NotFoundException([ + 'key' => $modelName . '.undefined' + ]); + } + + public function kirby() + { + return $this->kirby; + } + + public function language() + { + return $this->requestHeaders('x-language'); + } + + 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)); + } + + protected function setKirby(App $kirby) + { + $this->kirby = $kirby; + return $this; + } + + public function site() + { + return $this->kirby->site(); + } + + 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 + ] + ]); + } + + 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..09872ad --- /dev/null +++ b/kirby/src/Cms/App.php @@ -0,0 +1,1177 @@ +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' + ]); + + // setup the I18n class with the translation loader + $this->i18n(); + + // load all extensions + $this->extensionsFromSystem(); + $this->extensionsFromProps($props); + $this->extensionsFromPlugins(); + $this->extensionsFromOptions(); + $this->extensionsFromFolders(); + + // handle those damn errors + $this->handleErrors(); + + // set the singleton + Model::$kirby = static::$instance = $this; + + // 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 + * + * @return Api + */ + public function api(): 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 = $this->api ?? new Api($api); + } + + /** + * Apply a hook to the given value + * + * @param string $name + * @param mixed $value + * @return mixed + */ + public function apply(string $name, $value) + { + if ($functions = $this->extension('hooks', $name)) { + static $applied = []; + + foreach ($functions as $function) { + if (in_array($function, $applied[$name] ?? []) === true) { + continue; + } + + // mark the hook as triggered, to avoid endless loops + $applied[$name][] = $function; + + // bind the App object to the hook + $value = $function->call($this, $value); + } + } + + 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; + } + + /** + * Calls any Kirby route + * + * @return mixed + */ + public function call(string $path = null, string $method = null) + { + $path = $path ?? $this->path(); + $method = $method ?? $this->request()->method(); + $route = $this->router()->find($path, $method); + + $this->trigger('route:before', $route, $path, $method); + $result = $route->action()->call($route, ...$route->arguments()); + $this->trigger('route:after', $route, $path, $method, $result); + + return $result; + } + + /** + * Returns a specific user-defined collection + * by name. All relevant dependencies are + * automatically injected + * + * @param string $name + * @return void + */ + 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 Collections + */ + public function collections(): Collections + { + return $this->collections = $this->collections ?? Collections::load($this); + } + + /** + * Returns a core component + * + * @param string $name + * @return mixed + */ + public function component($name) + { + return $this->extensions['components'][$name] ?? null; + } + + /** + * Returns the content extension + * + * @return string + */ + public function contentExtension(): string + { + return $this->options['content']['extension'] ?? 'txt'; + } + + /** + * Returns files that should be ignored when scanning folders + * + * @return array + */ + public function contentIgnore(): array + { + return $this->options['content']['ignore'] ?? Dir::$ignore; + } + + /** + * Calls a page controller by name + * and with the given arguments + * + * @param string $name + * @param array $arguments + * @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 + * @return Closure|null + */ + protected function controllerLookup(string $name, string $contentType = 'html'): ?Controller + { + 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, Controller::class) ? $controller : new Controller($controller); + } + + return null; + } + + /** + * Returns the default language object + * + * @return Language|null + */ + public function defaultLanguage(): ?Language + { + return $this->defaultLanguage = $this->defaultLanguage ?? $this->languages()->default(); + } + + /** + * Destroy the instance singleton and + * purge other static props + */ + public static function destroy() + { + static::$plugins = []; + static::$instance = null; + } + + /** + * Detect the prefered language from the visitor object + * + * @return Language + */ + public function detectedLanguage() + { + $languages = $this->languages(); + $visitor = $this->visitor(); + + foreach ($visitor->acceptedLanguages() as $lang) { + if ($language = $languages->findBy('locale', $lang->locale())) { + return $language; + } + } + + foreach ($visitor->acceptedLanguages() as $lang) { + if ($language = $languages->findBy('code', $lang->code())) { + return $language; + } + } + + return $this->defaultLanguage(); + } + + /** + * Returns the Email singleton + * + * @return Email + */ + public function email($preset = [], array $props = []): Emailer + { + return new Emailer((new Email($preset, $props))->toArray(), $props['debug'] ?? false); + } + + /** + * Finds any file in the content directory + * + * @param string $path + * @param boolean $drafts + * @return File|null + */ + public function file(string $path, $parent = null, bool $drafts = true) + { + $parent = $parent ?? $this->site(); + $id = dirname($path); + $filename = basename($path); + + 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 self $instance + * @return self + */ + public static function instance(self $instance = null): self + { + 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 + * + * @param mixed $input + * @return 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')) { + $html = $input->render(); + + 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 + * + * @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 + * + * @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 + * + * @param string $text + * @param array $data + * @return string + */ + public function kirbytext(string $text = null, array $data = []): string + { + $text = $this->apply('kirbytext:before', $text); + $text = $this->kirbytags($text, $data); + $text = $this->markdown($text); + $text = $this->apply('kirbytext:after', $text); + + return $text; + } + + /** + * Returns the current language + * + * @param string|null $code + * @return Language|null + */ + public function language(string $code = null): ?Language + { + 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 + * + * @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 Languages + */ + public function languages(): Languages + { + return $this->languages = $this->languages ?? Languages::load(); + } + + /** + * Parses Markdown + * + * @param string $text + * @return string + */ + public function markdown(string $text = null): string + { + return $this->extensions['components']['markdown']($this, $text, $this->options['markdown'] ?? []); + } + + /** + * Check for a multilang setup + * + * @return boolean + */ + public function multilang(): bool + { + if ($this->multilang !== null) { + return $this->multilang; + } + + return $this->multilang = $this->languages()->count() !== 0; + } + + /** + * 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; + } + + /** + * Inject options from Kirby instance props + * + * @return array + */ + protected function optionsFromProps(array $options = []) + { + return $this->options = array_replace_recursive($this->options, $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); + } + + /** + * Returns any page from the content folder + * + * @param string $id + * @param Page|null $parent + * @param bool $drafts + * @return Page|null + */ + public function page(string $id, $parent = null, bool $drafts = true) + { + $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() + { + 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 + * + * @return Response + */ + public function render(string $path = null, string $method = null) + { + return $this->io($this->call($path, $method)); + } + + /** + * Returns the Request singleton + * + * @return Request + */ + public function request(): Request + { + return $this->request = $this->request ?? new Request; + } + + /** + * Path resolver for the router + * + * @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(); + + if ($path === null) { + return $site->homePage(); + } + + if ($page = $site->find($path)) { + return $page; + } + + if ($draft = $site->draft($path)) { + if ($this->user() || $draft->isVerified(get('token'))) { + return $draft; + } + } + + // try to resolve content representations if the path has an extension + $extension = F::extension($path); + + // remove the extension from the path + $path = Str::rtrim($path, '.' . $extension); + + // stop when there's no extension + if (empty($extension) === true) { + return null; + } + + // try to find the page for the representation + if ($page = $site->find($path)) { + try { + return $this + ->response() + ->body($page->render([], $extension)) + ->type($extension); + } catch (NotFoundException $e) { + return null; + } + } + + $id = dirname($path); + $filename = basename($path) . '.' . $extension; + + // 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 Responder + */ + public function response() + { + return $this->response = $this->response ?? new Responder; + } + + /** + * Returns all user roles + * + * @return Roles + */ + public function roles(): Roles + { + return $this->roles = $this->roles ?? Roles::load($this->root('roles')); + } + + /** + * Returns a system root + * + * @param string $type + * @return string + */ + public function root($type = 'index'): string + { + return $this->roots->__get($type); + } + + /** + * Returns the directory structure + * + * @return Ingredients + */ + public function roots(): Ingredients + { + return $this->roots; + } + + /** + * Returns the currently active route + * + * @return Route|null + */ + public function route() + { + return $this->router()->route(); + } + + /** + * Returns the Router singleton + * + * @return Router + */ + public function router(): Router + { + return $this->router = $this->router ?? new Router($this->routes()); + } + + /** + * Returns all defined routes + * + * @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); + + return $this->routes = array_merge($system['before'], $registry, $system['after']); + } + + /** + * Returns the current session object + * + * @param array $options Additional options, see the session component + * @return Session + */ + public function session(array $options = []) + { + $this->session = $this->session ?? new Session($this->root('sessions'), $this->options['session'] ?? []); + return $this->session->get($options); + } + + /** + * Create your own set of languages + * + * @param array $languages + * @return self + */ + protected function setLanguages(array $languages = null): self + { + if ($languages !== null) { + $this->languages = new Languages(); + + foreach ($languages as $props) { + $language = new Language($props); + $this->languages->data[$language->code()] = $language; + } + } + + 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; + } + + protected function setRequest(array $request = null): self + { + 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): self + { + if ($roles !== null) { + $this->roles = Roles::factory($roles, [ + 'kirby' => $this + ]); + } + + return $this; + } + + /** + * Sets a custom Site object + * + * @param array|Site $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 Server + */ + public function server(): Server + { + return $this->server = $this->server ?? new Server; + } + + /** + * Initializes and returns the Site object + * + * @return Site + */ + public function site(): 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 + * + * @param string $text + * @return string + */ + public function smartypants(string $text = null): string + { + return $this->extensions['components']['smartypants']($this, $text, $this->options['smartypants'] ?? []); + } + + /** + * Uses the snippet component to create + * and return a template snippet + * + * @return Snippet + */ + public function snippet(string $name, array $data = []): ?string + { + return $this->extensions['components']['snippet']($this, $name, array_merge($this->data, $data)); + } + + /** + * System check class + * + * @return System + */ + public function system(): System + { + return $this->system = $this->system ?? new System($this); + } + + /** + * Uses the template component to initialize + * and return the Template object + * + * @return Template + */ + public function template(string $name, string $type = 'html', string $defaultType = 'html'): Template + { + return $this->extensions['components']['template']($this, $name, $type, $defaultType); + } + + /** + * Thumbnail creator + * + * @param string $src + * @param string $dst + * @param array $options + * @return null + */ + public function thumb(string $src, string $dst, array $options = []) + { + return $this->extensions['components']['thumb']($this, $src, $dst, $options); + } + + /** + * Trigger a hook by name + * + * @param string $name + * @param mixed ...$arguments + * @return void + */ + public function trigger(string $name, ...$arguments) + { + if ($functions = $this->extension('hooks', $name)) { + static $triggered = []; + + foreach ($functions as $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); + } + } + } + + /** + * Returns a system url + * + * @param string $type + * @return string + */ + public function url($type = 'index'): string + { + return $this->urls->__get($type); + } + + /** + * Returns the url structure + * + * @return Ingredients + */ + public function urls(): Ingredients + { + return $this->urls; + } + + /** + * Returns the current version number from + * the composer.json (Keep that up to date! :)) + * + * @return string|null + */ + public static function version() + { + return static::$version = static::$version ?? Data::read(static::$root . '/composer.json')['version'] ?? null; + } + + /** + * Returns the visitor object + * + * @return Visitor + */ + public function visitor(): 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..4dfb857 --- /dev/null +++ b/kirby/src/Cms/AppCaches.php @@ -0,0 +1,112 @@ +caches[$key]) === true) { + return $this->caches[$key]; + } + + // get the options for this cache type + $options = $this->cacheOptions($key); + + if ($options['active'] === false) { + return $this->caches[$key] = new Cache; + } + + $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 + return $this->caches[$key] = new $className($options); + } + + /** + * 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 + ]; + } + + $defaults = [ + 'active' => true, + 'type' => 'file', + 'extension' => 'cache', + 'root' => $this->root('cache') . '/' . str_replace('.', '/', $key) + ]; + + 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 ($plugin = $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..bd9811f --- /dev/null +++ b/kirby/src/Cms/AppErrors.php @@ -0,0 +1,112 @@ +pushHandler(new PlainTextHandler); + $whoops->register(); + } + + protected function handleErrors() + { + $request = $this->request(); + + // TODO: implement acceptance + if ($request->ajax()) { + return $this->handleJsonErrors(); + } + + if ($request->cli()) { + return $this->handleCliErrors(); + } + + 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/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..62e4c4d --- /dev/null +++ b/kirby/src/Cms/AppPlugins.php @@ -0,0 +1,507 @@ + [], + 'blueprints' => [], + 'cacheTypes' => [], + 'collections' => [], + 'components' => [], + 'controllers' => [], + 'collectionFilters' => [], + 'fieldMethods' => [], + 'fileMethods' => [], + 'filesMethods' => [], + 'fields' => [], + 'hooks' => [], + 'options' => [], + 'pages' => [], + 'pageMethods' => [], + 'pageModels' => [], + 'pagesMethods' => [], + 'routes' => [], + 'sections' => [], + 'siteMethods' => [], + 'snippets' => [], + 'tags' => [], + 'templates' => [], + 'translations' => [], + 'validators' => [] + ]; + + /** + * Flag when plugins have been loaded + * to not load them again + * + * @var bool + */ + protected $pluginsAreLoaded = false; + + /** + * Register all given extensions + * + * @param array $extensions + * @param 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; + } + + protected function extendApi($api): array + { + if (is_array($api) === true) { + return $this->extensions['api'] = A::merge($this->extensions['api'], $api, A::MERGE_APPEND); + } else { + return $this->extensions['api']; + } + } + + protected function extendBlueprints(array $blueprints): array + { + return $this->extensions['blueprints'] = array_merge($this->extensions['blueprints'], $blueprints); + } + + protected function extendCacheTypes(array $cacheTypes): array + { + return $this->extensions['cacheTypes'] = array_merge($this->extensions['cacheTypes'], $cacheTypes); + } + + protected function extendCollectionFilters(array $filters): array + { + return $this->extensions['collectionFilters'] = Collection::$filters = array_merge(Collection::$filters, $filters); + } + + protected function extendCollections(array $collections): array + { + return $this->extensions['collections'] = array_merge($this->extensions['collections'], $collections); + } + + protected function extendComponents(array $components): array + { + return $this->extensions['components'] = array_merge($this->extensions['components'], $components); + } + + protected function extendControllers(array $controllers): array + { + return $this->extensions['controllers'] = array_merge($this->extensions['controllers'], $controllers); + } + + protected function extendFileMethods(array $methods): array + { + return $this->extensions['fileMethods'] = File::$methods = array_merge(File::$methods, $methods); + } + + protected function extendFilesMethods(array $methods): array + { + return $this->extensions['filesMethods'] = Files::$methods = array_merge(Files::$methods, $methods); + } + + protected function extendFieldMethods(array $methods): array + { + return $this->extensions['fieldMethods'] = Field::$methods = array_merge(Field::$methods, $methods); + } + + protected function extendFields(array $fields): array + { + return $this->extensions['fields'] = FormField::$types = array_merge(FormField::$types, $fields); + } + + 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']; + } + + protected function extendMarkdown(Closure $markdown): array + { + return $this->extensions['markdown'] = $markdown; + } + + 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); + } + + protected function extendPageMethods(array $methods): array + { + return $this->extensions['pageMethods'] = Page::$methods = array_merge(Page::$methods, $methods); + } + + protected function extendPagesMethods(array $methods): array + { + return $this->extensions['pagesMethods'] = Pages::$methods = array_merge(Pages::$methods, $methods); + } + + protected function extendPageModels(array $models): array + { + return $this->extensions['pageModels'] = Page::$models = array_merge(Page::$models, $models); + } + + 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::class) === true) { + $routes = $routes($this); + } + + return $this->extensions['routes'] = array_merge($this->extensions['routes'], $routes); + } + + protected function extendSections(array $sections): array + { + return $this->extensions['sections'] = Section::$types = array_merge(Section::$types, $sections); + } + + protected function extendSiteMethods(array $methods): array + { + return $this->extensions['siteMethods'] = Site::$methods = array_merge(Site::$methods, $methods); + } + + protected function extendSmartypants(Closure $smartypants): array + { + return $this->extensions['smartypants'] = $smartypants; + } + + protected function extendSnippets(array $snippets): array + { + return $this->extensions['snippets'] = array_merge($this->extensions['snippets'], $snippets); + } + + protected function extendTags(array $tags): array + { + return $this->extensions['tags'] = KirbyTag::$types = array_merge(KirbyTag::$types, $tags); + } + + protected function extendTemplates(array $templates): array + { + return $this->extensions['templates'] = array_merge($this->extensions['templates'], $templates); + } + + protected function extendTranslations(array $translations): array + { + return $this->extensions['translations'] = array_replace_recursive($this->extensions['translations'], $translations); + } + + 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 + * + * @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 + * + * @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 + * + * @return void + */ + protected function extensionsFromProps(array $props) + { + $this->extend($props); + } + + /** + * Apply all default extensions + * + * @return void + */ + protected function extensionsFromSystem() + { + // Form Field Mixins + FormField::$mixins['options'] = include static::$root . '/config/fields/mixins/options.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', + 'link' => 'toLink', + 'md' => 'markdown', + 'sp' => 'smartypants', + 'v' => 'isValid', + 'x' => 'xml' + ]; + + // default cache types + $this->extendCacheTypes([ + 'apcu' => 'Kirby\Cache\ApcuCache', + 'file' => 'Kirby\Cache\FileCache', + 'memcached' => 'Kirby\Cache\MemCache', + ]); + + $this->extendComponents(include static::$root . '/config/components.php'); + $this->extendBlueprints(include static::$root . '/config/blueprints.php'); + $this->extendFields(include static::$root . '/config/fields.php'); + $this->extendFieldMethods((include static::$root . '/config/methods.php')($this)); + $this->extendTags(include static::$root . '/config/tags.php'); + + // 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['headline'] = include static::$root . '/config/sections/mixins/headline.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'; + } + + /** + * 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 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. + * + * @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'); + $kirby = $this; + $loaded = []; + + foreach (Dir::read($root) as $dirname) { + if (in_array(substr($dirname, 0, 1), ['.', '_']) === true) { + continue; + } + + if (is_dir($root . '/' . $dirname) === false) { + continue; + } + + $dir = $root . '/' . $dirname; + $entry = $dir . '/index.php'; + + if (file_exists($entry) === false) { + 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..65ddeaa --- /dev/null +++ b/kirby/src/Cms/AppTranslations.php @@ -0,0 +1,127 @@ +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()); + } + + return $data; + }; + + I18n::$locale = function () { + if ($this->multilang() === true) { + return $this->defaultLanguage()->code(); + } else { + return 'en'; + } + }; + + I18n::$fallback = function () { + if ($this->multilang() === true) { + return $this->defaultLanguage()->code(); + } else { + return 'en'; + } + }; + + I18n::$translations = []; + } + + /** + * Load and set the current language if it exists + * Otherwise fall back to the default language + * + * @param string $languageCode + * @return Language|null + */ + public function setCurrentLanguage(string $languageCode = null) + { + if ($languageCode === null) { + return $this->language = null; + } + + if ($language = $this->language($languageCode)) { + $this->language = $language; + } else { + $this->language = $this->defaultLanguage(); + } + + if ($this->language) { + setlocale(LC_ALL, $this->language->locale()); + } + + return $this->language; + } + + /** + * Set the current translation + * + * @param string $translationCode + * @return void + */ + public function setCurrentTranslation(string $translationCode = null) + { + I18n::$locale = $translationCode ?? 'en'; + } + + /** + * Load a specific translation by locale + * + * @param string|null $locale + * @return 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('translations') . '/' . $locale . '.json', $inject); + } + + /** + * Returns all available translations + * + * @return Translations + */ + public function translations() + { + if (is_a($this->translations, 'Kirby\Cms\Translations') === true) { + return $this->translations; + } + + return Translations::load($this->root('translations'), $this->extensions['translations'] ?? []); + } +} diff --git a/kirby/src/Cms/AppUsers.php b/kirby/src/Cms/AppUsers.php new file mode 100755 index 0000000..7d2a594 --- /dev/null +++ b/kirby/src/Cms/AppUsers.php @@ -0,0 +1,99 @@ +auth = $this->auth ?? new Auth($this); + } + + /** + * Become any existing user + * + * @param string|null $who + * @return self + */ + public function impersonate(string $who = null) + { + return $this->auth()->impersonate($who); + } + + /** + * Set the currently active user id + * + * @param User|string $user + * @return self + */ + protected function setUser($user = null): self + { + $this->user = $user; + return $this; + } + + /** + * Create your own set of app users + * + * @param array $users + * @return self + */ + protected function setUsers(array $users = null): self + { + 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 + * @param \Kirby\Session\Session|array $session Session options or session object for getting the current user + * @return User|null + */ + public function user(string $id = null, $session = null) + { + if ($id !== null) { + return $this->users()->find($id); + } + + if (is_string($this->user) === true) { + return $this->auth()->impersonate($this->user); + } else { + return $this->auth()->user(); + } + } + + /** + * Returns all users + * + * @return Users + */ + public function users(): 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..8db96b5 --- /dev/null +++ b/kirby/src/Cms/Asset.php @@ -0,0 +1,11 @@ +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 BasicAuth|null $auth + * @return 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 + if ($request->ssl() === false) { + throw new PermissionException('Basic authentication is only allowed over HTTPS'); + } + + if ($user = $this->kirby->users()->find($auth->username())) { + if ($user->validatePassword($auth->password()) === true) { + return $user; + } + } + + return null; + } + + /** + * Returns the logged in user by checking + * the current session and finding a valid + * valid user id in there + * + * @param Session|null $session + * @return User|null + */ + public function currentUserFromSession($session = null) + { + // use passed session options or session object if set + if (is_array($session)) { + $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 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 + { + return hash('sha256', $this->kirby->visitor()->ip()); + } + + /** + * Check if logins are blocked for the current ip + * + * @return boolean + */ + public function isBlocked(): bool + { + $ip = $this->ipHash(); + $log = $this->log(); + $trials = $this->kirby->option('auth.trials', 10); + $timeout = $this->kirby->option('auth.timeout', 3600); + + if ($entry = ($log[$ip] ?? null)) { + if ($entry['trials'] > $trials) { + if ($entry['time'] > (time() - $timeout)) { + return true; + } + } + } + + return false; + } + + /** + * Login a user by email and password + * + * @param string $email + * @param string $password + * @param boolean $long + * @return User|false + */ + public function login(string $email, string $password, bool $long = false) + { + // check for blocked ips + if ($this->isBlocked() === true) { + throw new PermissionException('Rate limit exceeded', 403); + } + + // stop impersonating + $this->impersonate = null; + + // session options + $options = [ + 'createMode' => 'cookie', + 'long' => $long === true + ]; + + // validate the user and log in to the session + if ($user = $this->kirby->users()->find($email)) { + if ($user->login($password, $options) === true) { + return $this->user = $user; + } + } + + // log invalid login trial + $this->track(); + + // sleep for a random amount of milliseconds + // to make automated attacks harder + usleep(random_int(1000, 2000000)); + + return false; + } + + /** + * 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 { + return Data::read($this->logfile(), 'json'); + } catch (Throwable $e) { + return []; + } + } + + /** + * Logout the current user + * + * @return boolean + */ + public function logout(): bool + { + // stop impersonating + $this->impersonate = null; + + // logout the current user if it exists + if ($user = $this->user()) { + $user->logout(); + } + + $this->user = null; + return true; + } + + /** + * Tracks a login + * + * @return boolean + */ + public function track(): bool + { + $ip = $this->ipHash(); + $log = $this->log(); + $time = time(); + + if (isset($log[$ip]) === true) { + $log[$ip] = [ + 'time' => $time, + 'trials' => ($log[$ip]['trials'] ?? 0) + 1 + ]; + } else { + $log[$ip] = [ + '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 array|Session|null $session + * @return User|null + */ + public function user($session = null): ?User + { + if ($this->impersonate !== null) { + return $this->impersonate; + } + + try { + if ($this->type() === 'basic') { + return $this->user = $this->currentUserFromBasicAuth(); + } else { + return $this->user = $this->currentUserFromSession($session); + } + } catch (Throwable $e) { + return $this->user = null; + } + } +} diff --git a/kirby/src/Cms/Blueprint.php b/kirby/src/Cms/Blueprint.php new file mode 100755 index 0000000..ea73e5a --- /dev/null +++ b/kirby/src/Cms/Blueprint.php @@ -0,0 +1,759 @@ +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 = array_replace_recursive($mixin, $props); + } else { + try { + $props = array_replace_recursive(Data::read($mixin), $props); + } 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 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 + * @param string $fallback + * @param Model $model + * @return array + */ + public static function load(string $name) + { + 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 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) { + $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) + { + $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' => $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 = []; + } + + // 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) { + + // inject all section extensions + $sectionProps = $this->extend($sectionProps); + + $sections[$sectionName] = $sectionProps = array_merge($sectionProps, [ + 'name' => $sectionName, + 'type' => $type = $sectionProps['type'] ?? null + ]); + + if (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) { + + // 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 Section|null + */ + public function section(string $name): ?Section + { + 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..41aceb1 --- /dev/null +++ b/kirby/src/Cms/Collection.php @@ -0,0 +1,271 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + */ +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[$object->id()] = $object; + } + + /** + * Adds a single object or + * an entire second collection to the + * current collection + * + * @param mixed $item + */ + public function add($object) + { + if (is_a($object, static::class) === true) { + $this->data = array_merge($this->data, $object->data); + } else { + $this->__set($object->id(), $object); + } + + return $this; + } + + /** + * Appends an element to the data array + * + * @param mixed $key + * @param mixed $item + * @return Collection + */ + public function append(...$args) + { + if (count($args) === 1) { + if (is_object($args[0]) === true) { + $this->data[$args[0]->id()] = $args[0]; + } else { + $this->data[] = $args[0]; + } + } elseif (count($args) === 2) { + $this->set($args[0], $args[1]); + } + + return $this; + } + + /** + * Groups the items by a given field + * + * @param string $field + * @param bool $i (ignore upper/lowercase for group names) + * @return Collection A collection with an item for each group and a Collection for each group + */ + public function groupBy(string $field, bool $i = true) + { + $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 + * @return boolean + */ + 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 args any number of keys, passed as individual arguments + * @return 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 + * + * @return 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()); + } + + /** + * Returns the parent model + * + * @return Model + */ + public function parent() + { + return $this->parent; + } + + /** + * 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..6c98611 --- /dev/null +++ b/kirby/src/Cms/Collections.php @@ -0,0 +1,121 @@ +collection()` + * method to provide easy access to registered collections + * + * @package Kirby Cms + * @author Bastian Allgeier + * @link http://getkirby.com + * @copyright Bastian Allgeier + */ +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 Collection|null + */ + public function __call(string $name, array $arguments = []) + { + return $this->get($name, ...$arguments); + } + + /** + * Creates a new Collections set + * + * @param array $collections + */ + public function __construct(array $collections = []) + { + $this->collections = $collections; + } + + /** + * Loads a collection by name if registered + * + * @param string $name + * @param array $data + * @return Collection|null + */ + public function get(string $name, array $data = []) + { + if (isset($this->cache[$name]) === true) { + return $this->cache[$name]; + } + + if (isset($this->collections[$name]) === false) { + return null; + } + + $controller = new Controller($this->collections[$name]); + + return $this->cache[$name] = $controller->call(null, $data); + } + + /** + * Checks if a collection exists + * + * @param string $name + * @return boolean + */ + public function has(string $name): bool + { + return isset($this->collections[$name]) === true; + } + + /** + * Loads collections from php files in a + * given directory. + * + * @param string $root + * @return self + */ + public static function load(App $app): self + { + $collections = $app->extensions('collections'); + $root = $app->root('collections'); + + foreach (glob($root . '/*.php') as $file) { + $collection = require $file; + + if (is_a($collection, 'Closure')) { + $name = pathinfo($file, PATHINFO_FILENAME); + $collections[$name] = $collection; + } + } + + return new static($collections); + } +} diff --git a/kirby/src/Cms/Content.php b/kirby/src/Cms/Content.php new file mode 100755 index 0000000..bad3491 --- /dev/null +++ b/kirby/src/Cms/Content.php @@ -0,0 +1,222 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + */ +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 Page|File|User|Site + */ + protected $parent; + + /** + * Magic getter for content fields + * + * @param string $name + * @param array $arguments + * @return Field + */ + public function __call(string $name, array $arguments = []): Field + { + return $this->get($name); + } + + /** + * Creates a new Content object + * + * @param array $data + * @param object $parent + */ + public function __construct($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(); + } + + /** + * 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 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 boolean + */ + 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): self + { + $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 Site|Page|File|User + */ + public function parent() + { + return $this->parent; + } + + /** + * Set the parent model + * + * @param Model $parent + * @return self + */ + public function setParent(Model $parent): self + { + $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): self + { + $this->data = $overwrite === true ? (array)$content : array_merge($this->data, (array)$content); + return $this; + } +} diff --git a/kirby/src/Cms/ContentTranslation.php b/kirby/src/Cms/ContentTranslation.php new file mode 100755 index 0000000..5de6a74 --- /dev/null +++ b/kirby/src/Cms/ContentTranslation.php @@ -0,0 +1,230 @@ +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(); + $content = $this->content ?? $parent->readContent($this->code()); + + // merge with the default content + if ($this->isDefault() === false && $defaultLanguage = $parent->kirby()->defaultLanguage()) { + $default = $parent->translation($defaultLanguage->code())->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 boolean + */ + public function exists(): bool + { + return file_exists($this->contentFile()) === true; + } + + /** + * Returns the translation code as id + * + * @return void + */ + public function id() + { + return $this->code(); + } + + /** + * Checks if the this is the default translation + * of the model + * + * @return boolean + */ + 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 Page|File|Site + */ + public function parent() + { + return $this->parent; + } + + /** + * @param string $code + * @return self + */ + protected function setCode(string $code): self + { + $this->code = $code; + return $this; + } + + /** + * @param array $content + * @return self + */ + protected function setContent(array $content = null): self + { + $this->content = $content; + return $this; + } + + /** + * @param Model $parent + * @return self + */ + protected function setParent(Model $parent): self + { + $this->parent = $parent; + return $this; + } + + /** + * @param string $slug + * @return self + */ + protected function setSlug(string $slug = null): self + { + $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..620c9d2 --- /dev/null +++ b/kirby/src/Cms/Dir.php @@ -0,0 +1,177 @@ + [], + '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($dir, $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(string $dir, 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..6e2a977 --- /dev/null +++ b/kirby/src/Cms/Email.php @@ -0,0 +1,140 @@ + 'user', + 'replyTo' => 'user', + 'to' => 'user', + 'cc' => 'user', + 'bcc' => 'user', + 'attachments' => 'file' + ]; + + public function __construct($preset = [], array $props = []) + { + $this->options = $options = App::instance()->option('email'); + + // load presets from options + $this->preset = $this->preset($preset); + $this->props = array_merge($this->preset, $props); + + // add transport settings + $this->props['transport'] = $this->options['transport'] ?? []; + + // transform model objects to values + foreach (static::$transform as $prop => $model) { + $this->transformProp($prop, $model); + } + + // load template for body text + $this->template(); + } + + protected function preset($preset) + { + // only passed props, not preset name + if (is_string($preset) !== true) { + return $preset; + } + + // preset does not exist + if (isset($this->options['presets'][$preset]) === false) { + throw new NotFoundException([ + 'key' => 'email.preset.notFound', + 'data' => ['name' => $preset] + ]); + } + + return $this->options['presets'][$preset]; + } + + protected function template() + { + 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() && $text->exists()) { + $this->props['body'] = [ + 'html' => $html->render($data), + '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'); + } + } + } + + protected function getTemplate(string $name, string $type = null) + { + return App::instance()->template('emails/' . $name, $type, 'text'); + } + + public function toArray(): array + { + return $this->props; + } + + protected function transformFile($file) + { + return $this->transformModel($file, 'Kirby\Cms\File', 'root'); + } + + protected function transformModel($value, $class, $content) + { + // value is already a string + if (is_string($value) === true) { + return $value; + } + + // value is a model object, get value through content method + if (is_a($value, $class) === true) { + return $value->$content(); + } + + // value is an array or collection, call transform on each item + if (is_array($value) === true || is_a($value, 'Kirby\Cms\Collection') === true) { + $models = []; + foreach ($value as $model) { + $models[] = $this->transformModel($model, $class, $content); + } + return $models; + } + } + + protected function transformProp($prop, $model) + { + if (isset($this->props[$prop]) === true) { + $this->props[$prop] = $this->{'transform' . ucfirst($model)}($this->props[$prop]); + } + } + + protected function transformUser($user) + { + return $this->transformModel($user, 'Kirby\Cms\User', 'email'); + } +} diff --git a/kirby/src/Cms/Field.php b/kirby/src/Cms/Field.php new file mode 100755 index 0000000..54cb1a1 --- /dev/null +++ b/kirby/src/Cms/Field.php @@ -0,0 +1,246 @@ +myField()->lower(); + * ``` + * + * @package Kirby Cms + * @author Bastian Allgeier + * @link http://getkirby.com + * @copyright Bastian Allgeier + */ +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 Site|Page|File|User + */ + 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 = []) + { + if (isset(static::$methods[$method]) === true) { + return static::$methods[$method](clone $this, ...$arguments); + } + + if (isset(static::$aliases[$method]) === true) { + $method = 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 boolean + */ + public function exists(): bool + { + return $this->parent->content()->has($this->key); + } + + /** + * Checks if the field content is empty + * + * @return boolean + */ + public function isEmpty(): bool + { + return empty($this->value) === true; + } + + /** + * Checks if the field content is not empty + * + * @return boolean + */ + public function isNotEmpty(): bool + { + return empty($this->value) === false; + } + + /** + * Returns the name of the field + * + * @return string + */ + public function key(): string + { + return $this->key; + } + + /** + * 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 Page|File|Site|User + */ + 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 + * + * @param string|Closure $value + * @return mixed If a new value is passed, the modified + * field will be returned. Otherwise it + * will return the field value. + */ + 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..798207a --- /dev/null +++ b/kirby/src/Cms/File.php @@ -0,0 +1,867 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + */ +class File extends ModelWithContent +{ + use FileActions; + use FileFoundation; + use HasMethods; + use HasSiblings; + + /** + * The parent asset object + * This is used to do actual file + * method calls, like size, mime, etc. + * + * @var Image + */ + protected $asset; + + /** + * Cache for the initialized blueprint object + * + * @var FileBlueprint + */ + protected $blueprint; + + /** + * @var string + */ + protected $id; + + /** + * @var string + */ + protected $filename; + + /** + * All registered file methods + * + * @var array + */ + public static $methods = []; + + /** + * The parent object + * + * @var 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 + * + * @param bool $relative + * @return string + */ + public function apiUrl(bool $relative = false): string + { + return $this->parent()->apiUrl($relative) . '/files/' . $this->filename(); + } + + /** + * Returns the Asset object + * + * @return Image + */ + public function asset(): Image + { + return $this->asset = $this->asset ?? new Image($this->root()); + } + + /** + * Returns the FileBlueprint object for the file + * + * @return FileBlueprint + */ + public function blueprint(): FileBlueprint + { + if (is_a($this->blueprint, 'Kirby\Cms\FileBlueprint') === true) { + return $this->blueprint; + } + + return $this->blueprint = FileBlueprint::factory('files/' . $this->template(), 'files/default', $this); + } + + /** + * Blurs the image by the given amount of pixels + * + * @param boolean $pixels + * @return self + */ + public function blur($pixels = true) + { + return $this->thumb(['blur' => $pixels]); + } + + /** + * Converts the image to black and white + * + * @return self + */ + public function bw() + { + return $this->thumb(['grayscale' => true]); + } + + /** + * Store the template in addition to the + * other content. + * + * @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 + * + * @return string + */ + public function contentFileDirectory(): string + { + return dirname($this->root()); + } + + /** + * Filename for the content file + * + * @return string + */ + public function contentFileName(): string + { + return $this->filename(); + } + + /** + * Crops the image by the given width and height + * + * @param integer $width + * @param integer $height + * @param string|array $options + * @return self + */ + 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 + ]); + } + + /** + * Provides a kirbytag or markdown + * tag for the file, which will be + * used in the panel, when the file + * gets dragged onto a textarea + * + * @return string + */ + public function dragText($type = 'kirbytext'): string + { + switch ($type) { + case 'kirbytext': + if ($this->type() === 'image') { + return '(image: ' . $this->filename() . ')'; + } else { + return '(file: ' . $this->filename() . ')'; + } + // no break + case 'markdown': + if ($this->type() === 'image') { + return '![' . $this->alt() . '](./' . $this->filename() . ')'; + } else { + return '[' . $this->filename() . '](./' . $this->filename() . ')'; + } + } + } + + /** + * Checks if the file exists on disk + * + * @return boolean + */ + public function exists(): bool + { + return is_file($this->root()) === true; + } + + /** + * Returns the filename with extension + * + * @return string + */ + public function filename(): string + { + return $this->filename; + } + + /** + * Returns the parent Files collection + * + * @return Files + */ + public function files(): Files + { + return $this->siblingsCollection(); + } + + /** + * 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); + } + } + + /** + * 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 File $file + * @return bool + */ + public function is(File $file): bool + { + return $this->id() === $file->id(); + } + + /** + * Create a unique media hash + * + * @return string + */ + public function mediaHash(): string + { + return crc32($this->filename()) . '-' . $this->modified(); + } + + /** + * Returns the absolute path to the file in the public media folder + * + * @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 + * + * @return string + */ + public function mediaUrl(): string + { + return $this->parent()->mediaUrl() . '/' . $this->mediaHash() . '/' . $this->filename(); + } + + /** + * Alias for the old way of fetching File + * content. Nowadays `File::content()` should + * be used instead. + * + * @return Content + */ + public function meta(): Content + { + return $this->content(); + } + + /** + * Returns the parent model. + * This is normally the parent page + * or the site object. + * + * @return Site|Page + */ + public function model() + { + return $this->parent(); + } + + /** + * 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 parent Page object + * + * @return Page + */ + public function page() + { + return is_a($this->parent(), 'Kirby\Cms\Page') === true ? $this->parent() : null; + } + + /** + * Panel icon definition + * + * @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()] ?? []); + + $settings = [ + 'type' => $definition['type'] ?? 'file', + 'back' => 'pattern', + 'color' => $definition['color'] ?? $colorWhite, + 'ratio' => $params['ratio'] ?? null, + ]; + + return $settings; + } + + /** + * Panel image definition + * + * @param string|array|false $settings + * @param array $thumbSettings + * @return array + */ + public function panelImage($settings = null, array $thumbSettings = 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 + ]; + } + + $image = $this->query($settings['query'] ?? null, 'Kirby\Cms\File'); + + if ($image === null && $this->isViewable() === true) { + $image = $this; + } + + if ($image) { + $settings['url'] = $image->thumb($thumbSettings)->url(true); + unset($settings['query']); + } + + return array_merge($defaults, (array)$settings); + } + + /** + * Returns the full path without leading slash + * + * @return string + */ + public function panelPath(): string + { + return 'files/' . $this->filename(); + } + + /** + * Returns the url to the editing view + * in the panel + * + * @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 Model + */ + public function parent() + { + return $this->parent = $this->parent ?? $this->kirby()->site(); + } + + /** + * Returns the parent id if a parent exists + * + * @return string|null + */ + public function parentId(): ?string + { + if ($parent = $this->parent()) { + return $parent->id(); + } + + return null; + } + + /** + * Returns a collection of all parent pages + * + * @return Pages + */ + public function parents(): Pages + { + 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 FilePermissions + */ + public function permissions() + { + return new FilePermissions($this); + } + + /** + * Sets the JPEG compression quality + * + * @param integer $quality + * @return self + */ + public function quality(int $quality) + { + return $this->thumb(['quality' => $quality]); + } + + /** + * Creates a string query, starting from the model + * + * @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' => $this->site(), + 'file' => $this + ]); + + if ($expect !== null && is_a($result, $expect) !== true) { + return null; + } + + return $result; + } + + /** + * Resizes the file with the given width and height + * while keeping the aspect ratio. + * + * @param integer $width + * @param integer $height + * @param integer $quality + * @return self + */ + public function resize(int $width = null, int $height = null, int $quality = null) + { + return $this->thumb([ + 'width' => $width, + 'height' => $height, + 'quality' => $quality + ]); + } + + /** + * 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 FileRules + */ + protected function rules() + { + return new FileRules(); + } + + /** + * Sets the Blueprint object + * + * @param array|null $blueprint + * @return self + */ + protected function setBlueprint(array $blueprint = null): self + { + 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): self + { + $this->filename = $filename; + return $this; + } + + /** + * Sets the parent model object + * + * @param Model $parent + * @return self + */ + protected function setParent(Model $parent = null): self + { + $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): self + { + $this->template = $template; + return $this; + } + + /** + * Sets the url + * + * @param string $url + * @return self + */ + protected function setUrl(string $url = null): self + { + $this->url = $url; + return $this; + } + + /** + * Returns the parent Files collection + * + * @return Files + */ + protected function siblingsCollection() + { + return $this->parent()->files(); + } + + /** + * Returns the parent Site object + * + * @return Site + */ + public function site(): 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 self + */ + public function templateSiblings(bool $self = true) + { + return $this->siblings($self)->filterBy('template', $this->template()); + } + + /** + * 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 $options + * @return FileVersion|File + */ + public function thumb(array $options = null) + { + if (empty($options) === true) { + return $this; + } + + $result = $this->kirby()->component('file::version')($this->kirby(), $this, $options); + + if (is_a($result, FileVersion::class) === false && is_a($result, File::class) === false) { + throw new InvalidArgumentException('The file::version component must return a File or FileVersion object'); + } + + return $result; + } + + /** + * 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()); + } + + /** + * String template builder + * + * @param string|null $template + * @return string + */ + public function toString(string $template = null): string + { + if ($template === null) { + return $this->id(); + } + + return Str::template($template, [ + 'file' => $this, + 'site' => $this->site(), + 'kirby' => $this->kirby() + ]); + } + + /** + * 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..0e14bb4 --- /dev/null +++ b/kirby/src/Cms/FileActions.php @@ -0,0 +1,256 @@ +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 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 integer $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; + } + + /** + * 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): self + { + 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'])); + + // create the basic file and a test upload object + $file = new static($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) { + $file->unpublish(); + + 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(): self + { + Media::publish($this->root(), $this->mediaRoot()); + return $this; + } + + /** + * Alias for changeName + * + * @param string $name + * @param bool $sanitize + * @return self + */ + public function rename(string $name, bool $sanitize = true) + { + 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): self + { + 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(): self + { + 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..5c99260 --- /dev/null +++ b/kirby/src/Cms/FileBlueprint.php @@ -0,0 +1,65 @@ +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'] ?? []); + } + + public function accept(): array + { + return $this->props['accept']; + } + + protected function normalizeAccept($accept = null) + { + 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..20c95c5 --- /dev/null +++ b/kirby/src/Cms/FileFoundation.php @@ -0,0 +1,230 @@ +$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 Asset object + *^ + * @return Image + */ + public function asset(): Image + { + return $this->asset = $this->asset ?? new Image($this->root()); + } + + /** + * Checks if the file exists on disk + * + * @return boolean + */ + 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 boolean + */ + 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 boolean + */ + public function isViewable(): bool + { + $viewable = [ + 'jpg', + 'jpeg', + 'gif', + 'png', + 'svg', + 'webp' + ]; + + return in_array($this->extension(), $viewable) === true; + } + + /** + * Returns the paren app instance + * + * @return App + */ + public function kirby(): App + { + return App::instance(); + } + + /** + * 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() + { + 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/FilePermissions.php b/kirby/src/Cms/FilePermissions.php new file mode 100755 index 0000000..223734c --- /dev/null +++ b/kirby/src/Cms/FilePermissions.php @@ -0,0 +1,8 @@ +permissions()->changeName() !== true) { + throw new PermissionException([ + 'key' => 'file.changeName.permission', + 'data' => ['filename' => $file->filename()] + ]); + } + + $parent = $file->parent(); + $duplicate = $parent->files()->not($file)->findBy('name', $name); + + 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 LogicException('The file cannot be deleted'); + } + + return true; + } + + public static function replace(File $file, Image $upload): bool + { + if ($file->permissions()->replace() !== true) { + throw new LogicException('The file cannot be replaced'); + } + + static::validMime($file, $upload->mime()); + + + if ((string)$upload->mime() !== (string)$file->mime()) { + 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 LogicException('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..788a558 --- /dev/null +++ b/kirby/src/Cms/FileVersion.php @@ -0,0 +1,81 @@ +$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); + } + + // content fields + return $this->original()->content()->get($method, $arguments); + } + + public function id(): string + { + return dirname($this->original()->id()) . '/' . $this->filename(); + } + + public function kirby(): App + { + return $this->original()->kirby(); + } + + public function modifications(): array + { + return $this->modifications ?? []; + } + + public function original(): File + { + 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(File $original) + { + $this->original = $original; + } + + /** + * Convert the object to an array + * + * @return array + */ + public function toArray(): array + { + $array = array_merge(parent::toArray(), [ + '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..e5f86b5 --- /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 http://getkirby.com + * @copyright Bastian Allgeier + */ +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 intval($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 intval($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..7ff7eeb --- /dev/null +++ b/kirby/src/Cms/Files.php @@ -0,0 +1,132 @@ +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, File::class) === true) { + $this->__set($object->id(), $object); + } + + return $this; + } + + /** + * Sort all given files by the + * order in the array + * + * @param array $files + * @return self + */ + public function changeSort(array $files) + { + $index = 0; + + foreach ($files as $filename) { + if ($file = $this->get($filename)) { + $index++; + $file->changeSort($index); + } + } + + return $this; + } + + /** + * Creates a files collection from an array of props + * + * @param array $files + * @param Model $parent + * @param array $inject + * @return Files + */ + 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 = new File($props); + + $collection->data[$file->id()] = $file; + } + + return $collection; + } + + /** + * Tries to find a file by id/filename + * + * @param string $id + * @return File|null + */ + public function findById($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 File|null + */ + public function findByKey($key) + { + return $this->findById($key); + } + + /** + * Filter all files by the given template + * + * @param null|string|array $template + * @return self + */ + public function template($template): self + { + 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..0a75bcf --- /dev/null +++ b/kirby/src/Cms/Form.php @@ -0,0 +1,67 @@ +multilang() === true) { + $fields = $props['fields'] ?? []; + $isDefaultLanguage = $kirby->language()->isDefault(); + + 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); + } + + public static function for(Model $model, array $props = []) + { + // get the original model data + $original = $model->content()->toArray(); + + // set a few defaults + $props['values'] = array_merge($original, $props['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..e282a51 --- /dev/null +++ b/kirby/src/Cms/HasChildren.php @@ -0,0 +1,252 @@ +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 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 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 for the site + * + * @return Pages + */ + public function drafts(): Pages + { + 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 Pages + */ + public function find(...$arguments) + { + return $this->children()->find(...$arguments); + } + + /** + * Finds a single page or draft + * + * @return 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 Pages + */ + public function grandChildren(): Pages + { + return $this->children()->children(); + } + + /** + * Checks if the model has any children + * + * @return boolean + */ + public function hasChildren(): bool + { + return $this->children()->count() > 0; + } + + /** + * Checks if the model has any drafts + * + * @return boolean + */ + public function hasDrafts(): bool + { + return $this->drafts()->count() > 0; + } + + /** + * Deprecated! Use Page::hasUnlistedChildren + * + * @return boolean + */ + public function hasInvisibleChildren(): bool + { + return $this->children()->invisible()->count() > 0; + } + + /** + * Checks if the page has any listed children + * + * @return boolean + */ + public function hasListedChildren(): bool + { + return $this->children()->listed()->count() > 0; + } + + /** + * Checks if the page has any unlisted children + * + * @return boolean + */ + public function hasUnlistedChildren(): bool + { + return $this->children()->unlisted()->count() > 0; + } + + /** + * Deprecated! Use Page::hasListedChildren + * + * @return boolean + */ + public function hasVisibleChildren(): bool + { + return $this->children()->listed()->count() > 0; + } + + /** + * Creates a flat child index + * + * @param bool $drafts + * @return Pages + */ + public function index(bool $drafts = false): Pages + { + 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..582320a --- /dev/null +++ b/kirby/src/Cms/HasFiles.php @@ -0,0 +1,220 @@ +files()->filterBy('type', '==', 'audio'); + } + + /** + * Filters the Files collection by type code + * + * @return Files + */ + public function code(): Files + { + 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 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 Files + */ + public function documents(): Files + { + return $this->files()->filterBy('type', '==', 'document'); + } + + /** + * Returns a specific file by filename or the first one + * + * @param string $filename + * @param string $in + * @return File + */ + 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 Files + */ + public function files(): 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 File + */ + public function image(string $filename = null) + { + return $this->file($filename, 'images'); + } + + /** + * Filters the Files collection by type image + * + * @return Files + */ + public function images(): Files + { + return $this->files()->filterBy('type', '==', 'image'); + } + + /** + * Sets the Files collection + * + * @param Files|null $files + * @return self + */ + protected function setFiles(array $files = null): self + { + if ($files !== null) { + $this->files = Files::factory($files, $this); + } + + return $this; + } + + /** + * Filters the Files collection by type videos + * + * @return Files + */ + public function videos(): Files + { + 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..315bc65 --- /dev/null +++ b/kirby/src/Cms/HasMethods.php @@ -0,0 +1,38 @@ +call($this, ...$args); + } + + /** + * Checks if the object has a registered method + * + * @param string $method + * @return boolean + */ + 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..078a974 --- /dev/null +++ b/kirby/src/Cms/HasSiblings.php @@ -0,0 +1,133 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + */ +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 Model|null + */ + public function next() + { + return $this->siblingsCollection()->nth($this->indexOf() + 1); + } + + /** + * Returns the end of the collection starting after the current item + * + * @return Collection + */ + public function nextAll() + { + return $this->siblingsCollection()->slice($this->indexOf() + 1); + } + + /** + * Returns the previous item in the collection if available + * + * @return Model|null + */ + public function prev() + { + return $this->siblingsCollection()->nth($this->indexOf() - 1); + } + + /** + * Returns the beginning of the collection before the current item + * + * @return Collection + */ + public function prevAll() + { + return $this->siblingsCollection()->slice(0, $this->indexOf()); + } + + /** + * Returns all sibling elements + * + * @param bool $self + * @return 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 + * + * @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..f436124 --- /dev/null +++ b/kirby/src/Cms/Html.php @@ -0,0 +1,24 @@ +urls() and $kirby->roots() objects. + * Those are configured in `kirby/config/urls.php` + * and `kirby/config/roots.php` + */ +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 + * + * @param array $ingredients + * @return self + */ + public static function bake(array $ingredients): self + { + 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..f222869 --- /dev/null +++ b/kirby/src/Cms/KirbyTag.php @@ -0,0 +1,55 @@ +parent(); + + if (method_exists($parent, 'file') === true && $file = $parent->file($path)) { + return $file; + } + + if (is_a($parent, File::class) === true && $file = $parent->page()->file($path)) { + return $file; + } + + return $this->kirby()->file($path, null, true); + } + + /** + * Returns the current Kirby instance + * + * @return App + */ + public function kirby(): App + { + return $this->data['kirby'] ?? App::instance(); + } + + /** + * Returns the parent model + * + * @return Page|Site|File|User + */ + 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..deccb36 --- /dev/null +++ b/kirby/src/Cms/KirbyTags.php @@ -0,0 +1,55 @@ +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..28b5c1c --- /dev/null +++ b/kirby/src/Cms/Language.php @@ -0,0 +1,485 @@ +setRequiredProperties($props, [ + 'code' + ]); + + $this->setOptionalProperties($props, [ + 'default', + 'direction', + 'locale', + 'name', + '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 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 boolean + */ + protected static function converter(string $from, string $to): bool + { + $kirby = App::instance(); + $site = $kirby->site(); + + 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 + * + * @param array $props + * @return self + */ + public static function create(array $props): self + { + $props['code'] = Str::slug($props['code'] ?? null); + $kirby = App::instance(); + $languages = $kirby->languages(); + $site = $kirby->site(); + + // make the first language the default language + if ($languages->count() === 0) { + $props['default'] = true; + } + + $language = new static($props); + + if ($language->exists() === true) { + throw new DuplicateException('The language already exists'); + } + + $language->save(); + + if ($languages->count() === 0) { + static::converter('', $language->code()); + } + + return $language; + } + + /** + * Delete the current language and + * all its translation files + * + * @return boolean + */ + public function delete(): bool + { + if ($this->exists() === false) { + return true; + } + + $kirby = App::instance(); + $languages = $kirby->languages(); + $site = $kirby->site(); + $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. + * + * @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 boolean + */ + public function exists(): bool + { + return file_exists($this->root()); + } + + /** + * Checks if this is the default language + * for the site. + * + * @return boolean + */ + 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 string + * + * @return string + */ + public function locale(): string + { + return $this->locale; + } + + /** + * Returns the human-readable name + * of the language + * + * @return string + */ + public function name(): string + { + return $this->name; + } + + /** + * Returns the routing pattern for the language + * + * @return string + */ + public function pattern(): string + { + return $this->url; + } + + /** + * Returns the absolute path to the language file + * + * @return string + */ + public function root(): string + { + return App::instance()->root('languages') . '/' . $this->code() . '.php'; + } + + /** + * Saves the language settings in the languages folder + * + * @return self + */ + public function save(): self + { + $data = $this->toArray(); + + unset($data['url']); + + $export = 'root(), $export); + + return $this; + } + + /** + * @param string $code + * @return self + */ + protected function setCode(string $code): self + { + $this->code = $code; + return $this; + } + + /** + * @param boolean $default + * @return self + */ + protected function setDefault(bool $default = false): self + { + $this->default = $default; + return $this; + } + + /** + * @param string $direction + * @return self + */ + protected function setDirection(string $direction = 'ltr'): self + { + $this->direction = $direction === 'rtl' ? 'rtl' : 'ltr'; + return $this; + } + + /** + * @param string $locale + * @return self + */ + protected function setLocale(string $locale = null): self + { + $this->locale = $locale ?? $this->code; + return $this; + } + + /** + * @param string $name + * @return self + */ + protected function setName(string $name = null): self + { + $this->name = $name ?? $this->code; + return $this; + } + + /** + * @param array $translations + * @return self + */ + protected function setTranslations(array $translations = null): self + { + $this->translations = $translations ?? []; + return $this; + } + + /** + * @param string $url + * @return self + */ + protected function setUrl(string $url = null): self + { + $this->url = $url !== null ? trim($url, '/') : $this->code; + return $this; + } + + /** + * 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(), + '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 + { + return Url::to($this->url); + } + + /** + * Update language properties and save them + * + * @param array $props + * @return self + */ + public function update(array $props = null): self + { + $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/Languages.php b/kirby/src/Cms/Languages.php new file mode 100755 index 0000000..ff768ef --- /dev/null +++ b/kirby/src/Cms/Languages.php @@ -0,0 +1,83 @@ +keys(); + } + + /** + * Creates a new language with the given props + * + * @param array $props + * @return Language + */ + public function create(array $props): Language + { + return Language::create($props); + } + + /** + * Returns the default language + * + * @return Language|null + */ + public function default(): ?Language + { + if ($language = $this->findBy('isDefault', true)) { + return $language; + } else { + return $this->first(); + } + } + + /** + * Deprecated version of static::default(); + * + * @return Language|null + */ + public function findDefault(): ?Language + { + return $this->default(); + } + + /** + * Convert all defined languages to a collection + * + * @return self + */ + public static function load(): self + { + $languages = new static; + $files = glob(App::instance()->root('languages') . '/*.php'); + + foreach ($files as $file) { + $props = include_once $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); + + $language = new Language($props); + $languages->data[$language->code()] = $language; + } + } + + return $languages; + } +} diff --git a/kirby/src/Cms/Media.php b/kirby/src/Cms/Media.php new file mode 100755 index 0000000..23c4597 --- /dev/null +++ b/kirby/src/Cms/Media.php @@ -0,0 +1,136 @@ +kirby(); + $url = $model->mediaUrl() . '/' . $hash . '/' . $filename; + $root = $model->mediaRoot() . '/' . $hash; + $thumb = $root . '/' . $filename; + $job = $root . '/.jobs/' . $filename . '.json'; + $options = Data::read($job); + $file = $model->file($options['filename']); + + if (!$file || empty($options) === true) { + return false; + } + + $kirby->thumb($file->root(), $thumb, $options); + + F::remove($job); + + return Response::file($thumb); + } catch (Throwable $e) { + return false; + } + } + + /** + * Tries to find a file by model and filename + * and to copy it to the media folder. + * + * @param Model $model + * @param string $hash + * @param string $filename + * @return 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 boolean + */ + 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); + } + + /** + * 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..232f39e --- /dev/null +++ b/kirby/src/Cms/Model.php @@ -0,0 +1,105 @@ +id(); + } + + /** + * Each model must return a unique id + * + * @return string|int + */ + public function id() + { + return null; + } + + /** + * Returns the parent Kirby instance + * + * @return App|null + */ + public function kirby(): App + { + return static::$kirby = static::$kirby ?? App::instance(); + } + + /** + * Returns the parent Site instance + * + * @return Site|null + */ + public function site() + { + return $this->site = $this->site ?? $this->kirby()->site(); + } + + /** + * Setter for the parent Kirby object + * + * @param Kirby|null $kirby + * @return self + */ + protected function setKirby(App $kirby = null) + { + static::$kirby = $kirby; + return $this; + } + + /** + * Setter for the parent Site object + * + * @param 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..e6294bd --- /dev/null +++ b/kirby/src/Cms/ModelPermissions.php @@ -0,0 +1,68 @@ +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 + { + if ($this->user->role()->id() === 'nobody') { + return false; + } + + if (method_exists($this, 'can' . $action) === true && $this->{'can' . $action}() === false) { + return false; + } + + if (isset($this->options[$action]) === true && $this->options[$action] === false) { + return 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..0f1446a --- /dev/null +++ b/kirby/src/Cms/ModelWithContent.php @@ -0,0 +1,442 @@ +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 + * + * @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; + } + } + + /** + * Prepares the content that should be written + * to the text file + * + * @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 + * + * @return string|null + */ + public function contentFileDirectory(): ?string + { + return $this->root(); + } + + /** + * Returns the extension of the content file + * + * @return string + */ + public function contentFileExtension(): string + { + return $this->kirby()->contentExtension(); + } + + /** + * Needs to be declared by the final model + * + * @return string + */ + abstract public function contentFileName(): string; + + /** + * Decrement a given field value + * + * @param string $field + * @param integer $by + * @param integer $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 integer $by + * @param integer $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 data has any errors + * + * @return boolean + */ + public function isValid(): bool + { + return Form::for($this)->hasErrors() === false; + } + + /** + * Read the content from the content file + * + * @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 + */ + abstract public function root(): ?string; + + /** + * Stores the content on disk + * + * @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); + } + + // merge the translation with the new data + $translation->update($data, $overwrite); + + // 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 Content|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; + } + + /** + * Returns a single translation by language code + * If no code is specified the current translation is returned + * + * @param string $languageCode + * @return Translation|null + */ + public function translation(string $languageCode = null) + { + return $this->translations()->find($languageCode ?? $this->kirby()->language()->code()); + } + + /** + * Returns the translations collection + * + * @return 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 $language + * @param boolean $validate + * @return self + */ + public function update(array $input = null, string $languageCode = null, bool $validate = false) + { + $form = Form::for($this, [ + 'input' => $input, + 'ignoreDisabled' => $validate === false, + ]); + + // 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) { + return $model->save($strings, $languageCode, true); + }); + } + + /** + * Low level data writer method + * to store the given data on disk or anywhere else + * + * @param array $data + * @param string $languageCode + * @return boolean + */ + 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..a48d3df --- /dev/null +++ b/kirby/src/Cms/Nest.php @@ -0,0 +1,39 @@ + $value) { + if (is_array($value) === true) { + $result[$key] = static::create($value, $parent); + } elseif (is_string($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..5758a27 --- /dev/null +++ b/kirby/src/Cms/NestCollection.php @@ -0,0 +1,25 @@ +toArray(); + }); + } +} diff --git a/kirby/src/Cms/NestObject.php b/kirby/src/Cms/NestObject.php new file mode 100755 index 0000000..5baddf8 --- /dev/null +++ b/kirby/src/Cms/NestObject.php @@ -0,0 +1,35 @@ + $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..8718323 --- /dev/null +++ b/kirby/src/Cms/Page.php @@ -0,0 +1,1510 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + */ +class Page extends ModelWithContent +{ + 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 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 Template + */ + protected $intendedTemplate; + + /** + * @var array + */ + protected $inventory; + + /** + * The sorting number + * + * @var integer|null + */ + protected $num; + + /** + * The parent page + * + * @var Page|null + */ + protected $parent; + + /** + * Absolute path to the page directory + * + * @var string + */ + protected $root; + + /** + * The parent Site object + * + * @var 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 $args + * @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) + { + $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 + * + * @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 PageBlueprint + */ + public function blueprint(): PageBlueprint + { + 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 + * + * @return array + */ + public function blueprints(string $inSection = null): array + { + if ($inSection !== null) { + return $this->blueprint()->section($inSection)->blueprints(); + } + + $blueprints = []; + $templates = $this->blueprint()->options()['changeTemplate'] ?? false; + $currentTemplate = $this->intendedTemplate()->name(); + + // add the current template to the array + $templates[] = $currentTemplate; + + // make sure every template is only included once + $templates = array_unique($templates); + + // sort the templates + asort($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 + * + * @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 + * + * @param string $languageCode + * @return string + */ + public function contentFileName(string $languageCode = null): string + { + return $this->intendedTemplate()->name(); + } + + /** + * Call the page controller + * + * @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 integer + */ + 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 = $this->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 + * + * @return string + */ + public function dragText($type = 'kirbytext'): string + { + switch ($type) { + case 'kirbytext': + return '(link: ' . $this->id() . ' text: ' . $this->title() . ')'; + case 'markdown': + return '[' . $this->title() . '](' . $this->url() . ')'; + } + } + + /** + * 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. + * + * @return self + */ + public static function factory($props): self + { + 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 boolean + */ + 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 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 + * + * @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 Page|string $page + * @return bool + */ + public function is($page): bool + { + if (is_a($page, Page::class) === false) { + $page = $this->kirby()->page($page); + } + + if (is_a($page, Page::class) === 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 + * + * @return boolean + */ + 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 boolean + */ + 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 or special data + if (in_array($request->method(), ['GET', 'HEAD']) === false || empty($request->data()) === false) { + 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 string|Page $parent + * @return boolean + */ + public function isChildOf($parent): bool + { + return $this->parent()->is($parent); + } + + /** + * Checks if the page is a descendant of the given page + * + * @return boolean + */ + public function isDescendantOf(Page $parent): bool + { + return $this->parents()->has($parent->id()) === true; + } + + /** + * Checks if the page is a descendant of the currently active page + * + * @return boolean + */ + public function isDescendantOfActive(): bool + { + if ($active = $this->site()->page()) { + return $this->isDescendantOf($active); + } + + return false; + } + + /** + * Checks if the current page is a draft + * + * @return boolean + */ + 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 boolean + */ + 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 boolean + */ + public function isHomeOrErrorPage(): bool + { + return $this->isHomePage() === true || $this->isErrorPage() === true; + } + + /** + * Checks if the page is invisible + * + * @return bool + */ + public function isInvisible(): bool + { + return $this->isUnlisted(); + } + + /** + * Checks if the page has a sorting number + * + * @return boolean + */ + 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 boolean + */ + public function isSortable(): bool + { + return $this->permissions()->can('sort'); + } + + /** + * Checks if the page has no sorting number + * + * @return boolean + */ + public function isUnlisted(): bool + { + return $this->num() === null; + } + + /** + * Checks if the page access is verified. + * This is only used for drafts so far. + * + * @param string $token + * @return boolean + */ + public function isVerified(string $token = null) + { + if ($this->isDraft() === false && !$draft = $this->parents()->findBy('status', 'draft')) { + return true; + } + + if ($token === null) { + return false; + } + + return $this->token() === $token; + } + + /** + * Checks if the page is visible + * + * @return bool + */ + public function isVisible(): bool + { + return $this->isListed(); + } + + /** + * Returns the root to the media folder for the page + * + * @return string + */ + public function mediaRoot(): string + { + return $this->kirby()->root('media') . '/pages/' . $this->id(); + } + + /** + * The page's base url for any files + * + * @return string + */ + public function mediaUrl(): string + { + return $this->kirby()->url('media') . '/pages/' . $this->id(); + } + + /** + * Creates a Page model if it has been registered + * + * @param string $name + * @param array $props + * @return Page + */ + 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 = 'U', string $handler = null) + { + return F::modified($this->contentFile(), $format, $handler ?? $this->kirby()->option('date.handler', 'date')); + } + + /** + * Returns the sorting number + * + * @return integer|null + */ + public function num() + { + return $this->num; + } + + /** + * Returns the panel icon definition + * according to the blueprint settings + * + * @params array $params + * @return array + */ + public function panelIcon(array $params = null): array + { + if ($icon = $this->blueprint()->icon()) { + + // check for emojis + if (strlen($icon) !== Str::length($icon)) { + $options = [ + 'type' => $icon, + 'back' => 'black', + 'emoji' => true + ]; + } else { + $options = [ + 'type' => $icon, + 'back' => 'black', + ]; + } + } else { + $options = [ + 'type' => 'page', + 'back' => 'black', + ]; + } + + $options['ratio'] = $params['ratio'] ?? null; + + return $options; + } + + /** + * Returns the escaped Id, which is + * used in the panel to make routing work properly + * + * @return string + */ + public function panelId(): string + { + return str_replace('/', '+', $this->id()); + } + + /** + * @param string|array|false $settings + * @param array|null $thumbSettings + * @return array|null + */ + public function panelImage($settings = null, array $thumbSettings = 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->query($settings['query'] ?? 'page.image', 'Kirby\Cms\File')) { + $settings['url'] = $image->thumb($thumbSettings)->url(true) . '?t=' . $image->modified(); + + unset($settings['query']); + } + + return array_merge($defaults, (array)$settings); + } + + /** + * Returns the full path without leading slash + * + * @return string + */ + public function panelPath(): string + { + return 'pages/' . $this->panelId(); + } + + /** + * Returns the url to the editing view + * in the panel + * + * @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 Page|null + */ + public function parent() + { + return $this->parent; + } + + /** + * Returns the parent id, if a parent exists + * + * @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 + * + * @return Page|Site + */ + public function parentModel() + { + return $this->parent() ?? $this->site(); + } + + /** + * Returns a list of all parents and their parents recursively + * + * @return Pages + */ + public function parents(): Pages + { + $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 PagePermissions + */ + public function permissions() + { + return new PagePermissions($this); + } + + /** + * Draft preview Url + * + * @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) { + $url .= '?token=' . $this->token(); + } + + return $url; + } + + /** + * Creates a string query, starting from the model + * + * @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' => $this->site(), + 'page' => $this + ]); + + if ($expect !== null && is_a($result, $expect) !== true) { + return null; + } + + return $result; + } + + /** + * 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 integer $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; + } + + /** + * @return Template + */ + public function representation($type) + { + $kirby = $this->kirby(); + $template = $this->template(); + $representation = $kirby->template($template->name(), $type); + + if ($representation->exists() === true) { + return $representation; + } + + throw new NotFoundException('The content representation cannot be found'); + } + + /** + * Returns the absolute root to the page directory + * No matter if it exists or not. + * + * @return string + */ + public function root(): string + { + return $this->root = $this->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 PageRules + */ + protected function rules() + { + return new PageRules(); + } + + /** + * Search all pages within the current page + * + * @param string $query + * @param array $params + * @return 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): self + { + 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): self + { + $this->dirname = $dirname; + return $this; + } + + /** + * Sets the draft flag + * + * @param boolean $isDraft + * @return self + */ + protected function setIsDraft(bool $isDraft = null): self + { + $this->isDraft = $isDraft ?? false; + return $this; + } + + /** + * Sets the sorting number + * + * @param integer $num + * @return self + */ + protected function setNum(int $num = null): self + { + $this->num = $num === null ? $num : intval($num); + return $this; + } + + /** + * Sets the parent page object + * + * @param Page|null $parent + * @return self + */ + protected function setParent(Page $parent = null): self + { + $this->parent = $parent; + return $this; + } + + /** + * Sets the absolute path to the page + * + * @param string|null $root + * @return self + */ + protected function setRoot(string $root = null): self + { + $this->root = $root; + return $this; + } + + /** + * Sets the required Page slug + * + * @param string $slug + * @return self + */ + protected function setSlug(string $slug): self + { + $this->slug = $slug; + return $this; + } + + /** + * Sets the intended template + * + * @param string $template + * @return self + */ + protected function setTemplate(string $template = null): self + { + 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): self + { + 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() + { + if ($this->isDraft() === true) { + return 'draft'; + } + + if ($this->isUnlisted() === true) { + return 'unlisted'; + } + + return 'listed'; + } + + /** + * Returns the final template + * + * @return 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 Field + */ + public function title(): Field + { + 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()); + } + + /** + * String template builder + * + * @param string|null $template + * @return string + */ + public function toString(string $template = null): string + { + if ($template === null) { + return $this->id(); + } + + return Str::template($template, [ + 'page' => $this, + 'site' => $this->site(), + 'kirby' => $this->kirby() + ]); + } + + /** + * 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()) { + return $this->url = $this->parent()->url() . '/' . $this->uid(); + } + + return $this->url = $this->kirby()->url('base') . '/' . $this->uid(); + } + + /** + * Builds the Url for a specific language + * + * @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()) { + 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..eb6f1f8 --- /dev/null +++ b/kirby/src/Cms/PageActions.php @@ -0,0 +1,730 @@ +isDraft() === true) { + throw new LogicException('Drafts cannot change their sorting number'); + } + + 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) { + Dir::move($oldPage->root(), $newPage->root()); + } + + // 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 $language + * @return self + */ + public function changeSlug(string $slug, string $languageCode = null): self + { + // 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 + ]); + + // actually move stuff on disk + if ($oldPage->exists() === true) { + if (Dir::move($oldPage->root(), $newPage->root()) !== true) { + throw new LogicException('The page directory cannot be moved'); + } + + 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 $language + * @return self + */ + protected function changeSlugForLanguage(string $slug, string $languageCode = null): self + { + $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 integer $position Optional sorting number + * @return Page + */ + public function changeStatus(string $status, int $position = null): self + { + 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(): self + { + $page = $this->commit('changeStatus', [$this, 'draft'], function ($page) { + return $page->unpublish(); + }); + + return $page; + } + + protected function changeStatusToListed(int $position = null): self + { + // 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; + } + + protected function changeStatusToUnlisted(): self + { + if ($this->status() === 'unlisted') { + return $this; + } + + $page = $this->commit('changeStatus', [$this, 'unlisted'], 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): self + { + if ($template === $this->template()->name()) { + return $this; + } + + // prepare data to transfer between blueprints + $oldBlueprint = 'pages/' . $this->template(); + $newBlueprint = 'pages/' . $template; + + return $this->commit('changeTemplate', [$this, $template], function ($oldPage, $template) use ($oldBlueprint, $newBlueprint) { + if ($this->kirby()->multilang() === true) { + $newPage = $this->clone([ + 'template' => $template + ]); + + foreach ($this->kirby()->languages()->codes() as $code) { + if (F::remove($oldPage->contentFile($code)) !== true) { + throw new LogicException('The old text file could not be removed'); + } + + // convert the content to the new blueprint + $content = $oldPage->transferData($oldPage->content($code), $oldBlueprint, $newBlueprint)['data']; + + // save the language file + $newPage->save($content, $code); + } + + // return a fresh copy of the object + return $newPage->clone(); + } else { + $newPage = $this->clone([ + 'content' => $this->transferData($this->content(), $oldBlueprint, $newBlueprint)['data'], + '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): self + { + 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 mixed ...$arguments + * @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; + } + + /** + * Creates and stores a new page + * + * @param array $props + * @return self + */ + public static function create(array $props): self + { + // 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): self + { + $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 integer $num + * @return integer + */ + public function createNum(int $num = null): int + { + $mode = $this->blueprint()->num(); + + switch ($mode) { + case 'zero': + return 0; + case 'date': + case 'datetime': + $format = 'date' ? 'Ymd' : 'YmdHi'; + $date = $this->content()->get('date')->value(); + $time = empty($date) === true ? time() : strtotime($date); + + return date($format, $time); + 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: + $template = Str::template($mode, [ + 'kirby' => $this->kirby(), + 'page' => $this, + 'site' => $this->site(), + ]); + + return intval($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; + }); + } + + 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 + */ + public function purge(): self + { + $this->children = null; + $this->blueprint = null; + $this->drafts = null; + $this->files = null; + $this->content = null; + $this->inventory = 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', 'desc'); + } + + return true; + } + + public function sort($position = null) + { + return $this->changeStatus('listed', $position); + } + + /** + * Transfers data from old to new blueprint and tracks changes + * + * @param Content $content + * @param string $old Old blueprint + * @param string $new New blueprint + * @return array + */ + protected function transferData(Content $content, string $old, string $new): array + { + // Prepare data + $data = []; + $old = Blueprint::factory($old, 'pages/default', $this); + $new = Blueprint::factory($new, 'pages/default', $this); + $oldForm = new Form(['fields' => $old->fields(), 'model' => $this]); + $newForm = new Form(['fields' => $new->fields(), 'model' => $this]); + $oldFields = $oldForm->fields(); + $newFields = $newForm->fields(); + + // Tracking changes + $added = []; + $replaced = []; + $removed = []; + + // Ensure to keep title + $data['title'] = $content->get('title')->value(); + + // Go through all fields of new template + foreach ($newFields as $newField) { + $name = $newField->name(); + $oldField = $oldFields->get($name); + + // Field name matches with old template + if ($oldField !== null) { + + // Same field type, add and keep value + if ($oldField->type() === $newField->type()) { + $data[$name] = $content->get($name)->value(); + + // Different field type, add with empty value + } else { + $data[$name] = null; + $replaced[$name] = $oldFields->get($name)->label() ?? $name; + } + + // Field does not exist in old template, + // add with empty or preserved value + } else { + $preserved = $content->get($name); + $data[$name] = $preserved ? $preserved->value(): null; + $added[$name] = $newField->label() ?? $name; + } + } + + // Go through all values to preserve them + foreach ($content->fields() as $field) { + $name = $field->key(); + $newField = $newFields->get($name); + + if ($newField === null) { + $data[$name] = $field->value(); + $removed[$name] = $field->name(); + } + } + + return [ + 'data' => $data, + 'added' => $added, + 'replaced' => $replaced, + 'removed' => $removed + ]; + } + + /** + * 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 boolean $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..796576c --- /dev/null +++ b/kirby/src/Cms/PageBlueprint.php @@ -0,0 +1,196 @@ +props['options'] = $this->normalizeOptions( + $props['options'] ?? true, + // defaults + [ + 'changeSlug' => null, + 'changeStatus' => null, + 'changeTemplate' => null, + 'changeTitle' => null, + 'create' => null, + 'delete' => 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', + '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; + } + + 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|boolean + */ + 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..b60b64f --- /dev/null +++ b/kirby/src/Cms/PagePermissions.php @@ -0,0 +1,53 @@ +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/PageRules.php b/kirby/src/Cms/PageRules.php new file mode 100755 index 0000000..026d4fd --- /dev/null +++ b/kirby/src/Cms/PageRules.php @@ -0,0 +1,241 @@ + '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 ($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']); + } + + $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']); + } + + if (($page->hasChildren() === true || $page->hasDrafts() === true) && $force === false) { + throw new LogicException(['key' => 'page.delete.hasChildren']); + } + + 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..0bdff30 --- /dev/null +++ b/kirby/src/Cms/PageSiblings.php @@ -0,0 +1,188 @@ +hasNextUnlisted(); + } + + /** + * Checks if there's a next listed + * page in the siblings collection + * + * @return bool + */ + public function hasNextListed(): bool + { + return $this->nextListed() !== null; + } + + /** + * @deprecated Use Page::hasNextListed instead + * @return boolean + */ + public function hasNextVisible(): bool + { + return $this->hasNextListed(); + } + + /** + * Checks if there's a next unlisted + * page in the siblings collection + * + * @return bool + */ + public function hasNextUnlisted(): bool + { + return $this->nextUnlisted() !== null; + } + + /** + * @deprecated Use Page::hasPrevUnlisted instead + * @return boolean + */ + public function hasPrevInvisible(): bool + { + 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 Use Page::hasPrevListed instead + * @return boolean + */ + public function hasPrevVisible(): bool + { + return $this->hasPrevListed(); + } + + /** + * @deprecated Use Page::nextUnlisted() instead + * @return self|null + */ + public function nextInvisible() + { + return $this->nextUnlisted(); + } + + /** + * Returns the next listed page if it exists + * + * @return self|null + */ + public function nextListed() + { + return $this->nextAll()->listed()->first(); + } + + /** + * Returns the next unlisted page if it exists + * + * @return self|null + */ + public function nextUnlisted() + { + return $this->nextAll()->unlisted()->first(); + } + + /** + * @deprecated Use Page::prevListed() instead + * @return self|null + */ + public function nextVisible() + { + return $this->nextListed(); + } + + /** + * @deprecated Use Page::prevUnlisted() instead + * @return self|null + */ + public function prevInvisible() + { + return $this->prevUnlisted(); + } + + /** + * Returns the previous listed page + * + * @return self|null + */ + public function prevListed() + { + return $this->prevAll()->listed()->last(); + } + + /** + * Returns the previous unlisted page + * + * @return self|null + */ + public function prevUnlisted() + { + return $this->prevAll()->unlisted()->first(); + } + + /** + * @deprecated Use Page::prevListed() instead + * @return self|null + */ + public function prevVisible() + { + return $this->prevListed(); + } + + /** + * Private siblings collector + * + * @return 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 self + */ + 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..f68c01b --- /dev/null +++ b/kirby/src/Cms/Pages.php @@ -0,0 +1,482 @@ + 'project-a']), + * new Page(['id' => 'project-b']), + * new Page(['id' => 'project-c']), + * ]); + * ``` + * + * @package Kirby Cms + * @author Bastian Allgeier + * @link http://getkirby.com + * @copyright Bastian Allgeier + */ +class Pages extends Collection +{ + + /** + * Cache for the index + * + * @var null|Pages + */ + 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 $item + * @return Pages + */ + 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, Page::class) === true) { + $this->__set($object->id(), $object); + } + + return $this; + } + + /** + * Returns all audio files of all children + * + * @return Files + */ + public function audio(): Files + { + return $this->files()->filterBy("type", "audio"); + } + + /** + * Returns all children for each page in the array + * + * @return Pages + */ + public function children(): Pages + { + $children = new Pages([], $this->parent); + + foreach ($this->data as $pageKey => $page) { + foreach ($page->children() as $childKey => $child) { + $children->data[$childKey] = $child; + } + } + + return $children; + } + + /** + * Returns all code files of all children + * + * @return Files + */ + public function code(): Files + { + return $this->files()->filterBy("type", "code"); + } + + /** + * Returns all documents of all children + * + * @return Files + */ + public function documents(): Files + { + return $this->files()->filterBy("type", "document"); + } + + /** + * Fetch all drafts for all pages in the collection + * + * @return Pages + */ + public function drafts() + { + $drafts = new Pages([], $this->parent); + + foreach ($this->data as $pageKey => $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 Model $parent + * @param array $inject + * @param bool $draft + * @return Pages + */ + 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 Files + */ + public function files(): Files + { + $files = new Files([], $this->parent); + + foreach ($this->data as $pageKey => $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 $id + * @return mixed + */ + public function findById($id) + { + $id = trim($id, '/'); + $page = $this->get($id); + $multiLang = App::instance()->multilang(); + + if ($multiLang === true) { + $page = $this->findBy('slug', $id); + } + + if (!$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 $startAt + * @return mixed + */ + public function findByIdRecursive($id, $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 $key + * @return mixed + */ + public function findByKey($key) + { + return $this->findById($key); + } + + /** + * Alias for Pages::findById + * + * @param string $id + * @return Page|null + */ + public function findByUri(string $id) + { + return $this->findById($id); + } + + /** + * Finds the currently open page + * + * @return Page|null + */ + public function findOpen() + { + return $this->findBy('isOpen', true); + } + + /** + * Custom getter that is able to find + * extension pages + * + * @param string $key + * @return 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 Files + */ + public function images(): Files + { + return $this->files()->filterBy("type", "image"); + } + + /** + * Create a recursive flat index of all + * pages and subpages, etc. + * + * @param bool $drafts + * @return Pages + */ + public function index(bool $drafts = false): Pages + { + 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 alias for Pages::unlisted() + * + * @return self + */ + public function invisible(): self + { + return $this->filterBy('isUnlisted', '==', true); + } + + /** + * Returns all listed pages in the collection + * + * @return self + */ + public function listed(): self + { + return $this->filterBy('isListed', '==', true); + } + + /** + * Returns all unlisted pages in the collection + * + * @return self + */ + public function unlisted(): self + { + return $this->filterBy('isUnlisted', '==', true); + } + + /** + * Include all given items in the collection + * + * @return self + */ + public function merge(...$args): self + { + // 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; + } + + /** + * 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 self + */ + public function published(): self + { + return $this->filterBy('isDraft', '==', false); + } + + /** + * Filter all pages by the given template + * + * @param null|string|array $template + * @return self + */ + public function template($templates): self + { + 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 Files + */ + public function videos(): Files + { + return $this->files()->filterBy("type", "video"); + } + + /** + * Deprecated alias for Pages::listed() + * + * @return self + */ + public function visible(): self + { + return $this->filterBy('isListed', '==', true); + } +} diff --git a/kirby/src/Cms/Pagination.php b/kirby/src/Cms/Pagination.php new file mode 100755 index 0000000..c6743f1 --- /dev/null +++ b/kirby/src/Cms/Pagination.php @@ -0,0 +1,172 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + */ +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'], 1); + } else { + $params['page'] = $params['page'] ?? $params['url']->params()->get($params['variable'], 1); + } + + 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 + */ + public function nextPageUrl() + { + 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. + * + * @return string|null + */ + public function pageUrl(int $page = null) + { + 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 + */ + public function prevPageUrl() + { + 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..713ea6f --- /dev/null +++ b/kirby/src/Cms/Panel.php @@ -0,0 +1,94 @@ +root('media') . '/panel'; + $panelRoot = $kirby->root('panel') . '/dist'; + $versionHash = md5($kirby->version()); + $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($kirby->root('panel') . '/dist', $versionRoot) !== true) { + throw new Exception('Panel assets could not be linked'); + } + + return true; + } + + /** + * Renders the main panel view + * + * @param App $kirby + * @return Response + */ + public static function render(App $kirby): Response + { + try { + if (static::link($kirby) === true) { + usleep(1); + go($kirby->request()->url()); + } + } catch (Throwable $e) { + die('The panel assets cannot be installed properly. Please check permissions of your media folder.'); + } + + $view = new View($kirby->root('kirby') . '/views/panel.php', [ + 'kirby' => $kirby, + 'config' => $kirby->option('panel'), + 'assetUrl' => $kirby->url('media') . '/panel/' . md5($kirby->version()), + 'pluginCss' => $kirby->url('media') . '/plugins/index.css', + 'pluginJs' => $kirby->url('media') . '/plugins/index.js', + 'icons' => F::read($kirby->root('panel') . '/dist/img/icons.svg'), + 'panelUrl' => $url = $kirby->url('panel'), + 'options' => [ + 'url' => $url, + 'site' => $kirby->url('index'), + 'api' => $kirby->url('api'), + 'csrf' => $kirby->option('api')['csrf'] ?? csrf(), + 'translation' => 'en', + 'debug' => true, + 'search' => [ + 'limit' => $kirby->option('panel')['search']['limit'] ?? 10 + ] + ] + ]); + + return new Response($view->render()); + } +} diff --git a/kirby/src/Cms/Permissions.php b/kirby/src/Cms/Permissions.php new file mode 100755 index 0000000..2f7b960 --- /dev/null +++ b/kirby/src/Cms/Permissions.php @@ -0,0 +1,161 @@ + [ + 'panel' => true, + 'users' => true, + 'site' => 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, + '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_bool($settings) === true) { + return $this->setAll($settings); + } + + if (is_array($settings) === true) { + return $this->setCategories($settings); + } + } + + public function for(string $category = null, string $action = null) + { + 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) + { + return $this->hasCategory($category) === true && array_key_exists($action, $this->actions[$category]) === true; + } + + protected function hasCategory(string $category) + { + 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/Plugin.php b/kirby/src/Cms/Plugin.php new file mode 100755 index 0000000..3185cce --- /dev/null +++ b/kirby/src/Cms/Plugin.php @@ -0,0 +1,106 @@ +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; + } + + 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..aedaea9 --- /dev/null +++ b/kirby/src/Cms/PluginAssets.php @@ -0,0 +1,115 @@ +root('media') . '/plugins/.index.' . $extension; + $build = false; + $modified = [0]; + $assets = []; + + foreach ($kirby->plugins() as $plugin) { + $file = $plugin->root() . '/index.' . $extension; + + if (file_exists($file) === true) { + $assets[] = $file; + $modified[] = F::modified($file); + } + } + + if (empty($assets)) { + return false; + } + + if (file_exists($cache) === false || filemtime($cache) < max($modified)) { + $dist = []; + foreach ($assets as $asset) { + $dist[] = file_get_contents($asset); + } + $dist = implode(PHP_EOL, $dist); + F::write($cache, $dist); + } else { + $dist = file_get_contents($cache); + } + + return $dist; + } + + /** + * Clean old/deprecated assets on every resolve + * + * @param string $pluginName + * @return void + */ + public static function clean(string $pluginName) + { + 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 string + */ + 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; + + F::link($source, $target, 'symlink'); + + return $url; + } + } + + return null; + } +} diff --git a/kirby/src/Cms/R.php b/kirby/src/Cms/R.php new file mode 100755 index 0000000..06e06cf --- /dev/null +++ b/kirby/src/Cms/R.php @@ -0,0 +1,20 @@ +request(); + } +} diff --git a/kirby/src/Cms/Responder.php b/kirby/src/Cms/Responder.php new file mode 100755 index 0000000..253446a --- /dev/null +++ b/kirby/src/Cms/Responder.php @@ -0,0 +1,218 @@ +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 integer $code + * @return integer|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 + * @return self + */ + public function fromArray(array $response) + { + $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 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 integer|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 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..8810a09 --- /dev/null +++ b/kirby/src/Cms/Response.php @@ -0,0 +1,29 @@ +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() + { + return [ + 'admin' => [ + 'description' => 'The admin has all rights', + 'name' => 'admin', + 'title' => 'Admin', + 'permissions' => true, + ], + 'nobody' => [ + 'description' => 'This is a fallback role without any permissions', + 'name' => 'nobody', + 'title' => 'Nobody', + 'permissions' => false, + ] + ]; + } + + public function description() + { + return $this->description; + } + + public static function factory(array $props, array $inject = []): self + { + 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'; + } + + public static function load(string $file, array $inject = []): self + { + $name = F::name($file); + + $data = Data::read($file); + $data['name'] = $name; + + return static::factory($data, $inject); + } + + public function name(): string + { + return $this->name; + } + + public static function nobody(array $inject = []) + { + try { + return static::load('nobody'); + } catch (Exception $e) { + return static::factory(static::defaults()['nobody'], $inject); + } + } + + public function permissions(): Permissions + { + return $this->permissions; + } + + protected function setDescription(string $description = null): self + { + $this->description = $description; + return $this; + } + + protected function setName(string $name): self + { + $this->name = $name; + return $this; + } + + protected function setPermissions($permissions = null): self + { + $this->permissions = new Permissions($permissions); + return $this; + } + + protected function setTitle($title = null): self + { + $this->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..aab9543 --- /dev/null +++ b/kirby/src/Cms/Roles.php @@ -0,0 +1,76 @@ +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'); + } + + public static function load(string $root = null, array $inject = []): self + { + $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 (Dir::read($root) as $filename) { + if ($filename === 'default.yml') { + continue; + } + + $role = Role::load($root . '/' . $filename, $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..a7bdfad --- /dev/null +++ b/kirby/src/Cms/S.php @@ -0,0 +1,20 @@ +session(); + } +} diff --git a/kirby/src/Cms/Search.php b/kirby/src/Cms/Search.php new file mode 100755 index 0000000..dddeda1 --- /dev/null +++ b/kirby/src/Cms/Search.php @@ -0,0 +1,111 @@ +site()->index()->files()->search($query, $params); + } + + /** + * Native search method to search for anything within the collection + */ + 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 (empty($options['fields']) === false) { + $keys = array_intersect($keys, $options['fields']); + } + + $item->searchHits = 0; + $item->searchScore = 0; + + foreach ($keys as $key) { + $score = $options['score'][$key] ?? 1; + $value = $key === 'id' ? $item->id() : $data[$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'); + } + + public static function pages(string $query = null, $params = []) + { + return App::instance()->site()->index()->search($query, $params); + } + + 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..1212431 --- /dev/null +++ b/kirby/src/Cms/Section.php @@ -0,0 +1,67 @@ +model->kirby(); + } + + 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..201be24 --- /dev/null +++ b/kirby/src/Cms/Site.php @@ -0,0 +1,675 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + */ +class Site extends ModelWithContent +{ + 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 + * + * @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 SiteBlueprint + */ + public function blueprint(): SiteBlueprint + { + 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 + * + * @params 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 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 + * + * @return array + */ + public function contentFileData(array $data, string $languageCode = null): array + { + return A::prepend($data, [ + 'title' => $data['title'] ?? null, + ]); + } + + /** + * Filename for the content file + * + * @return string + */ + public function contentFileName(): string + { + return 'site'; + } + + /** + * Returns the error page object + * + * @return Page + */ + 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 + * + * @return string + */ + public function errorPageId(): string + { + return $this->errorPageId ?? 'error'; + } + + /** + * Checks if the site exists on disk + * + * @return boolean + */ + public function exists(): bool + { + return is_dir($this->root()) === true; + } + + /** + * Returns the home page object + * + * @return Page + */ + 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 + * + * @return string + */ + public function homePageId(): string + { + return $this->homePageId ?? 'home'; + } + + /** + * Creates an inventory of all files + * and children in the site directory + * + * @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() + ); + } + + /** + * Returns the root to the media folder for the site + * + * @return string + */ + public function mediaRoot(): string + { + return $this->kirby()->root('media') . '/site'; + } + + /** + * The site's base url for any files + * + * @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 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 Pages + */ + public function pages(): Pages + { + return $this->children(); + } + + /** + * Returns the full path without leading slash + * + * @return string + */ + public function panelPath(): string + { + return 'site'; + } + + /** + * Returns the url to the editing view + * in the panel + * + * @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 SitePermissions + */ + public function permissions() + { + return new SitePermissions($this); + } + + /** + * Creates a string query, starting from the model + * + * @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' => $this, + ]); + + if ($expect !== null && is_a($result, $expect) !== true) { + return null; + } + + return $result; + } + + /** + * 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 SiteRules + */ + protected function rules() + { + return new SiteRules(); + } + + /** + * Search all pages in the site + * + * @param string $query + * @param array $params + * @return 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): self + { + 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'): self + { + $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'): self + { + $this->homePageId = $id; + return $this; + } + + /** + * Sets the current page object + * + * @param Page|null $page + * @return self + */ + public function setPage(Page $page = null): self + { + $this->page = $page; + return $this; + } + + /** + * Sets the Url + * + * @param string $url + * @return void + */ + protected function setUrl($url = null): self + { + $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(), + ]; + } + + /** + * String template builder + * + * @param string|null $template + * @return string + */ + public function toString(string $template = null): string + { + if ($template === null) { + return $this->url(); + } + + return Str::template($template, [ + 'site' => $this, + 'kirby' => $this->kirby() + ]); + } + + /** + * 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 + * + * @params string $languageCode + * @params 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 + * + * @param string|Page $page + * @param string|null $languageCode + * @return Page + */ + public function visit($page, string $languageCode = null): Page + { + 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 + * + * @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..bf6a859 --- /dev/null +++ b/kirby/src/Cms/SiteActions.php @@ -0,0 +1,85 @@ +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): self + { + 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 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 + */ + public function purge(): self + { + $this->children = null; + $this->blueprint = null; + $this->files = null; + $this->content = null; + $this->inventory = null; + + return $this; + } +} diff --git a/kirby/src/Cms/SiteBlueprint.php b/kirby/src/Cms/SiteBlueprint.php new file mode 100755 index 0000000..879832d --- /dev/null +++ b/kirby/src/Cms/SiteBlueprint.php @@ -0,0 +1,29 @@ +props['options'] = $this->normalizeOptions( + $props['options'] ?? true, + // defaults + [ + 'changeTitle' => null, + 'update' => null, + ], + // aliases + [ + 'title' => 'changeTitle', + ] + ); + } +} diff --git a/kirby/src/Cms/SitePermissions.php b/kirby/src/Cms/SitePermissions.php new file mode 100755 index 0000000..b3c09d1 --- /dev/null +++ b/kirby/src/Cms/SitePermissions.php @@ -0,0 +1,8 @@ +permissions()->changeTitle() !== true) { + throw new PermissionException(['key' => 'site.changeTitle.permission']); + } + + 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..94a06ee --- /dev/null +++ b/kirby/src/Cms/Structure.php @@ -0,0 +1,59 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + */ +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 $object + */ + public function __set(string $id, $props) + { + if (is_a($props, StructureObject::class) === true) { + $object = $props; + } else { + $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..be1a9cc --- /dev/null +++ b/kirby/src/Cms/StructureObject.php @@ -0,0 +1,206 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + */ +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 Content + */ + public function content(): 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 a formatted date field from the content + * + * @param string $format + * @param string $field + * @return Field + */ + public function date(string $format = null, $field = 'date') + { + return $this->content()->get($field)->toDate($format); + } + + /** + * Returns the required id + * + * @return string + */ + public function id(): string + { + return $this->id; + } + + /** + * Returns the parent Model object + * + * @return Page|Site|File|User + */ + 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): self + { + $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): self + { + $this->id = $id; + return $this; + } + + /** + * Sets the parent Model. This can either be a + * Page, Site, File or User object + * + * @param Page|Site|File|User|null $parent + * @return self + */ + protected function setParent(Model $parent = null): self + { + $this->parent = $parent; + return $this; + } + + /** + * Sets the parent Structure collection + * + * @param Structure $structure + * @return self + */ + protected function setStructure(Structure $structure = null): self + { + $this->structure = $structure; + return $this; + } + + /** + * Returns the parent Structure collection as siblings + * + * @return 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..266728f --- /dev/null +++ b/kirby/src/Cms/System.php @@ -0,0 +1,446 @@ +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 boolean + */ + public function accounts(): bool + { + return is_writable($this->app->root('accounts')); + } + + /** + * Check for a writable content folder + * + * @return boolean + */ + public function content(): bool + { + return is_writable($this->app->root('content')); + } + + /** + * Check for an existing curl extension + * + * @return boolean + */ + public function curl(): bool + { + return extension_loaded('curl'); + } + + /** + * Create the most important folders + * if they don't exist yet + * + * @return void + */ + public function init() + { + /* /site/accounts */ + try { + Dir::make($this->app->root('accounts')); + } catch (Throwable $e) { + throw new PermissionException('The accounts directory could not be created'); + } + + /* /content */ + try { + Dir::make($this->app->root('content')); + } catch (Throwable $e) { + throw new PermissionException('The content directory could not be created'); + } + + 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 boolean + */ + public function isInstallable(): bool + { + return $this->isLocal() === true || $this->app->option('panel.install', false) === true; + } + + /** + * Check if Kirby is already installed + * + * @return boolean + */ + public function isInstalled(): bool + { + return $this->app->users()->count() > 0; + } + + /** + * Check if this is a local installation + * + * @return boolean + */ + 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 boolean + */ + public function isOk(): bool + { + if ($this->isInstallable() === false) { + return false; + } + + return in_array(false, array_values($this->status()), true) === false; + } + + /** + * Returns the app's index URL for + * licensing purposes without scheme + * + * @return string + */ + protected function licenseUrl(): 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(); + } + + /** + * 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 licenseUrlNormalized(string $url = null): string + { + if ($url === null) { + $url = $this->licenseUrl(); + } + + // 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() + { + 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->licenseUrlNormalized() !== $this->licenseUrlNormalized($license['domain'])) { + return false; + } + + return $license['license']; + } + + /** + * Check for an existing mbstring extension + * + * @return boolean + */ + public function mbString(): bool + { + return extension_loaded('mbstring'); + } + + /** + * Check for a writable media folder + * + * @return boolean + */ + public function media(): bool + { + return is_writable($this->app->root('media')); + } + + /** + * Check for a valid PHP version + * + * @return boolean + */ + 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 boolean + */ + public function register(string $license, string $email): bool + { + $response = Remote::get('https://licenses.getkirby.com/register', [ + 'data' => [ + 'license' => $license, + 'email' => $email, + 'domain' => $this->licenseUrl() + ] + ]); + + 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 Exception('The license could not be verified'); + } + + return true; + } + + /** + * Check for a valid server environment + * + * @return boolean + */ + 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 boolean + */ + 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..fc5d059 --- /dev/null +++ b/kirby/src/Cms/Template.php @@ -0,0 +1,198 @@ +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 boolean + */ + 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) { + // + } + + // 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 boolean + */ + 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..a11e19d --- /dev/null +++ b/kirby/src/Cms/Translation.php @@ -0,0 +1,171 @@ +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 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..051b83f --- /dev/null +++ b/kirby/src/Cms/Translations.php @@ -0,0 +1,55 @@ +parent->contentFile('', true), $this->parent->contentFile($code, true)); + } + + public function stop(string $code) + { + F::move($this->parent->contentFile($code, true), $this->parent->contentFile('', true)); + } + + 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; + } + + 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..dd0f18b --- /dev/null +++ b/kirby/src/Cms/Url.php @@ -0,0 +1,45 @@ +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; + } +} diff --git a/kirby/src/Cms/User.php b/kirby/src/Cms/User.php new file mode 100755 index 0000000..2b27d65 --- /dev/null +++ b/kirby/src/Cms/User.php @@ -0,0 +1,787 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + */ +class User extends ModelWithContent +{ + use HasFiles; + 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; + + /** + * @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; + } + + // 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 + * + * @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 File|null + */ + public function avatar() + { + return $this->files()->template('avatar')->first(); + } + + /** + * Returns the UserBlueprint object + * + * @return 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 + * + * @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 + * + * @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 boolean + */ + public function exists(): bool + { + return is_file($this->contentFile('default')) === true; + } + + /** + * Hashes user password + * + * @param string|null $password + * @return string|null + */ + public static function hashPassword($password) + { + if ($password !== null) { + $info = password_get_info($password); + + if ($info['algo'] === 0) { + $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 User $user + * @return bool + */ + public function is(User $user): bool + { + return $this->id() === $user->id(); + } + + /** + * Checks if this user has the admin role + * + * @return boolean + */ + public function isAdmin(): bool + { + return $this->role()->id() === 'admin'; + } + + /** + * Checks if the current user is the virtual + * Kirby user + * + * @return boolean + */ + public function isKirby(): bool + { + return $this->email() === 'kirby@getkirby.com'; + } + + /** + * Checks if the current user is this user + * + * @return boolean + */ + public function isLoggedIn(): bool + { + return $this->is($this->kirby()->user()); + } + + /** + * Checks if the user is the last one + * with the admin role + * + * @return boolean + */ + 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 boolean + */ + 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 Session|array $session Session options or session object to set the user in + * @return bool + */ + public function login(string $password, $session = null): bool + { + try { + $this->validatePassword($password); + } catch (Exception $e) { + throw new PermissionException(['key' => 'access.login']); + } + + $this->loginPasswordless($session); + + return true; + } + + /** + * Logs the user in without checking the password + * + * @param Session|array $session Session options or session object to set the user in + * @return void + */ + public function loginPasswordless($session = null) + { + $session = $this->sessionFromOptions($session); + + $session->regenerateToken(); // privilege change + $session->data()->set('user.id', $this->id()); + } + + /** + * Logs the user out + * + * @param Session|array $session Session options or session object to unset the user in + * @return void + */ + public function logout($session = null) + { + $session = $this->sessionFromOptions($session); + + $session->data()->remove('user.id'); + + if ($session->data()->get() === []) { + // session is now empty, we might as well destroy it + $session->destroy(); + } else { + // privilege change + $session->regenerateToken(); + } + } + + /** + * Returns the root to the media folder for the user + * + * @return string + */ + public function mediaRoot(): string + { + return $this->kirby()->root('media') . '/users/' . $this->id(); + } + + /** + * Returns the media url for the user object + * + * @return string + */ + public function mediaUrl(): string + { + return $this->kirby()->url('media') . '/users/' . $this->id(); + } + + /** + * 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 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 + * + * @return self + */ + public static function nobody(): self + { + return new static([ + 'email' => 'nobody@getkirby.com', + 'role' => 'nobody' + ]); + } + + /** + * Returns the full path without leading slash + * + * @return string + */ + public function panelPath(): string + { + return 'users/' . $this->id(); + } + + /** + * Returns the url to the editing view + * in the panel + * + * @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 UserPermissions + */ + public function permissions() + { + return new UserPermissions($this); + } + + /** + * Creates a string query, starting from the model + * + * @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' => $kirby = $this->kirby(), + 'site' => $kirby->site(), + 'user' => $this + ]); + + if ($expect !== null && is_a($result, $expect) !== true) { + return null; + } + + return $result; + } + + /** + * Returns the user role + * + * @return string + */ + public function role(): 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 UserRules + */ + protected function rules() + { + return new UserRules(); + } + + /** + * Sets the Blueprint object + * + * @param array|null $blueprint + * @return self + */ + protected function setBlueprint(array $blueprint = null): self + { + 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): self + { + 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): self + { + $this->id = $id; + return $this; + } + + /** + * Sets the user language + * + * @param string $language + * @return self + */ + protected function setLanguage(string $language = null): self + { + $this->language = $language !== null ? trim($language) : null; + return $this; + } + + /** + * Sets the user name + * + * @param string $name + * @return self + */ + protected function setName(string $name = null): self + { + $this->name = $name !== null ? trim($name) : null; + return $this; + } + + /** + * Sets and hashes a new user password + * + * @param string $password + * @return self + */ + protected function setPassword(string $password = null): self + { + $this->password = $password; + return $this; + } + + /** + * Sets the user role + * + * @param string $role + * @return self + */ + protected function setRole(string $role = null): self + { + $this->role = $role !== null ? strtolower(trim($role)) : null; + return $this; + } + + /** + * Converts session options into a session object + * + * @param Session|array $session Session options or session object to unset the user in + * @return Session + */ + protected function sessionFromOptions($session): 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 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) { + return $this->email(); + } + + return Str::template($template, [ + 'user' => $this, + 'site' => $this->site(), + 'kirby' => $this->kirby() + ]); + } + + /** + * Returns the username + * which is the given name or the email + * as a fallback + * + * @return string + */ + public function username(): ?string + { + return $this->name()->or($this->email())->value(); + } + + /** + * Compares the given password with the stored one + * + * @param string $password + * @return boolean + */ + public function validatePassword(string $password = null): bool + { + if (empty($this->password()) === true) { + throw new NotFoundException(['key' => 'user.password.undefined']); + } + + if ($password === null) { + throw new InvalidArgumentException(['key' => 'user.password.invalid']); + } + + if (password_verify($password, $this->password()) !== true) { + throw new InvalidArgumentException(['key' => 'user.password.invalid']); + } + + return true; + } +} diff --git a/kirby/src/Cms/UserActions.php b/kirby/src/Cms/UserActions.php new file mode 100755 index 0000000..6d0a078 --- /dev/null +++ b/kirby/src/Cms/UserActions.php @@ -0,0 +1,295 @@ +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): self + { + 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): self + { + 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): self + { + 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): self + { + 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 $input + * @return self + */ + public static function create(array $props = null): self + { + $data = $props; + + if (isset($props['password']) === true) { + $data['password'] = static::hashPassword($props['password']); + } + + $user = new static($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()); + + // write the user data + return $user->save(); + }); + } + + /** + * 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'); + } + + 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 + * + * @return boolean + */ + protected function writeCredentials(array $credentials): bool + { + $export = 'root() . '/index.php', $export); + } + + /** + * 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..273e55f --- /dev/null +++ b/kirby/src/Cms/UserBlueprint.php @@ -0,0 +1,31 @@ +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..ce3728d --- /dev/null +++ b/kirby/src/Cms/UserPermissions.php @@ -0,0 +1,26 @@ +category = $this->user && $this->user->is($model) ? 'user' : 'users'; + } + + protected function canChangeRole(): bool + { + return $this->model->isLastAdmin() !== true; + } + + protected function canDelete(): bool + { + return $this->model->isLastAdmin() !== true; + } +} diff --git a/kirby/src/Cms/UserRules.php b/kirby/src/Cms/UserRules.php new file mode 100755 index 0000000..818e897 --- /dev/null +++ b/kirby/src/Cms/UserRules.php @@ -0,0 +1,206 @@ +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 + { + 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']); + } + + 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 ($duplicate = $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..75ee978 --- /dev/null +++ b/kirby/src/Cms/Users.php @@ -0,0 +1,108 @@ +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, User::class) === 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 = []): self + { + $collection = new static; + + // read all user blueprints + foreach ($users as $props) { + $user = new User($props + $inject); + $collection->set($user->id(), $user); + } + + return $collection; + } + + /** + * Finds a user in the collection by id or email address + * + * @param string $key + * @return User|null + */ + public function findByKey($key) + { + if (Str::contains($key, '@') === true) { + return parent::findBy('email', $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 = []): self + { + $users = new static; + + foreach (Dir::read($root) as $userDirectory) { + if (is_dir($root . '/' . $userDirectory) === false) { + continue; + } + + $user = new User([ + 'id' => $userDirectory, + ] + $inject); + + $users->set($user->id(), $user); + } + + return $users; + } +} diff --git a/kirby/src/Cms/Visitor.php b/kirby/src/Cms/Visitor.php new file mode 100755 index 0000000..341fd79 --- /dev/null +++ b/kirby/src/Cms/Visitor.php @@ -0,0 +1,19 @@ +visitor(); + } +} diff --git a/kirby/src/Data/Data.php b/kirby/src/Data/Data.php new file mode 100755 index 0000000..727c484 --- /dev/null +++ b/kirby/src/Data/Data.php @@ -0,0 +1,124 @@ + + * @link http://getkirby.com + * @copyright 2012 Bastian Allgeier + * @license MIT + */ +class Data +{ + + /** + * Handler Type Aliases + * + * @var array + */ + public static $aliases = [ + 'yml' => 'yaml', + 'md' => 'txt', + 'mdown' => 'txt' + ]; + + /** + * All registered handlers + * + * @var array + */ + public static $handlers = [ + 'json' => 'Kirby\Data\Json', + 'yaml' => 'Kirby\Data\Yaml', + 'txt' => 'Kirby\Data\Txt' + ]; + + /** + * Handler getter + * + * @param string $type + * @return Handler|null + */ + public static function handler(string $type) + { + // normalize the type + $type = strtolower($type); + $handler = static::$handlers[$type] ?? null; + + if ($handler === null && isset(static::$aliases[$type]) === true) { + $handler = static::$handlers[static::$aliases[$type]] ?? null; + } + + if ($handler === null) { + throw new Exception('Missing Handler for type: "' . $type . '"'); + } + + return new $handler; + } + + /** + * Decode 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); + } + + /** + * Encode 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 boolean + */ + 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..b9e6032 --- /dev/null +++ b/kirby/src/Data/Handler.php @@ -0,0 +1,67 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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 array $data + * @return string + */ + abstract public static function encode(array $data): string; + + /** + * Reads data from a file + * + * @param string $file + * @return array + */ + public static function read(string $file): array + { + if (file_exists($file) !== true) { + throw new Exception('The file "' . $file . '" does not exist'); + } + + return static::decode(F::read($file)); + } + + /** + * Writes data to a file. + * The data handler is automatically chosen by + * the extension if not specified. + * + * @param array $data + * @return boolean + */ + 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..53757f1 --- /dev/null +++ b/kirby/src/Data/Json.php @@ -0,0 +1,45 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license MIT + */ +class Json extends Handler +{ + /** + * Converts an array to an encoded JSON string + * + * @param array $data + * @return string + */ + public static function encode(array $data): string + { + return json_encode($data); + } + + /** + * Parses an encoded JSON string and returns a multi-dimensional array + * + * @param string $string + * @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/Txt.php b/kirby/src/Data/Txt.php new file mode 100755 index 0000000..d3ba749 --- /dev/null +++ b/kirby/src/Data/Txt.php @@ -0,0 +1,112 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license MIT + */ +class Txt extends Handler +{ + + /** + * Converts an array to an encoded Kirby txt string + * + * @param array $data + * @return string + */ + public static function encode(array $data): string + { + $result = []; + + foreach ($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 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); + } + + // escape accidental dividers within a field + $value = preg_replace('!(\n|^)----(.*?\R*)!', '$1\\----$2', $value); + + return $value; + } + + /** + * Helper for converting key and value to 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"; + } + + $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; + } + + $data[$key] = trim(substr($field, $pos + 1)); + } + + return $data; + } +} diff --git a/kirby/src/Data/Yaml.php b/kirby/src/Data/Yaml.php new file mode 100755 index 0000000..783f638 --- /dev/null +++ b/kirby/src/Data/Yaml.php @@ -0,0 +1,56 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license MIT + */ +class Yaml extends Handler +{ + + /** + * Converts an array to an encoded YAML string + * + * @param array $data + * @return string + */ + public static function encode(array $data): string + { + // $data, $indent, $wordwrap, $no_opening_dashes + return Spyc::YAMLDump($data, false, false, true); + } + + /** + * Parses an encoded YAML string and returns a multi-dimensional array + * + * @param string $string + * @return array + */ + public static function decode($yaml): array + { + if ($yaml === null) { + return []; + } + + if (is_array($yaml) === true) { + return $yaml; + } + + $result = Spyc::YAMLLoadString($yaml); + + if (is_array($result)) { + return $result; + } else { + throw new Exception('YAML string is invalid'); + } + } +} diff --git a/kirby/src/Database/Database.php b/kirby/src/Database/Database.php new file mode 100755 index 0000000..5107477 --- /dev/null +++ b/kirby/src/Database/Database.php @@ -0,0 +1,621 @@ +connect($params); + } + + /** + * Returns one of the started instance + * + * @param string $id + * @return Database + */ + public static function instance(string $id = null): self + { + 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 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 Database|null + */ + public function connection() + { + return $this->connection; + } + + /** + * Sets the exception mode for the next query + * + * @param boolean $fail + * @return 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; + } + + /** + * Private method to execute database queries. + * This is used by the query() and execute() methods + * + * @param string $query + * @param array $bindings + * @return boolean + */ + 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 boolean + */ + 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 Sql + */ + public function sql() + { + $className = static::$types[$this->type]['sql'] ?? 'Sql'; + return new $className($this); + } + + /** + * Sets the current table, which should be queried + * + * @param string $table + * @return Query Returns a Query object, which can be used to build a full query for that table + */ + 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 boolean + */ + 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 boolean + */ + 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 boolean + */ + 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 boolean + */ + 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() + */ + 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..ffdc5f1 --- /dev/null +++ b/kirby/src/Database/Db.php @@ -0,0 +1,262 @@ + 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 Database + */ + public static function connection() + { + return static::$connection; + } + + /** + * Sets the current table, which should be queried + * + * @param string $table + * @return Query Returns a Query object, which can be used to build a full query for that table + */ + 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 boolean +*/ +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 boolean + */ +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 boolean + */ +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..daefe72 --- /dev/null +++ b/kirby/src/Database/Query.php @@ -0,0 +1,1055 @@ +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 boolean $debug + * @return Query + */ + public function debug(bool $debug = true) + { + $this->debug = $debug; + return $this; + } + + /** + * Enables distinct select clauses. + * + * @param boolean $distinct + * @return 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 boolean $fail + * @return 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 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 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 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 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 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 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 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 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 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 list + * @return 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 list + * @return 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 list + * @return 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 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 list + * @return Query + */ + public function having(...$args) + { + $this->having = $this->filterQuery($args, $this->having); + return $this; + } + + /** + * Attaches an order clause + * + * @param string $order + * @return Query + */ + public function order(string $order = null) + { + $this->order = $order; + return $this; + } + + /** + * Sets the offset for select clauses + * + * @param int $offset + * @return Query + */ + public function offset(int $offset = null) + { + $this->offset = $offset; + return $this; + } + + /** + * Sets the limit for select clauses + * + * @param int $limit + * @return 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 Query + */ + public function count() + { + return $this->aggregate('COUNT'); + } + + /** + * Builds a max query + * + * @param string $column + * @return Query + */ + public function max(string $column) + { + return $this->aggregate('MAX', $column); + } + + /** + * Builds a min query + * + * @param string $column + * @return Query + */ + public function min(string $column) + { + return $this->aggregate('MIN', $column); + } + + /** + * Builds a sum query + * + * @param string $column + * @return Query + */ + public function sum(string $column) + { + return $this->aggregate('SUM', $column); + } + + /** + * Builds an average query + * + * @param string $column + * @return 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(array($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 boolean + */ + 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 boolean + */ + 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..d0cac73 --- /dev/null +++ b/kirby/src/Database/Sql.php @@ -0,0 +1,929 @@ +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->database, + $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 boolean $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 + * + * @params 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 integer $offset + * @param integer|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 array($table, $this->unquoteIdentifier($parts[0])); + + // qualified identifier + case 2: + return array($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->database + ] + ]; + } + + /** + * 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 boolean + */ + 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 boolean $set If true builds a set list of values for update clauses + * @param boolean $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..113d0ab --- /dev/null +++ b/kirby/src/Database/Sql/Mysql.php @@ -0,0 +1,12 @@ + '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..9602e57 --- /dev/null +++ b/kirby/src/Email/Body.php @@ -0,0 +1,51 @@ +, + * Nico Hoffmann + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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(): string + { + return $this->text; + } + + protected function setHtml(string $html = null) + { + $this->html = $html; + return $this; + } + + protected function setText(string $text) + { + $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..373d2a7 --- /dev/null +++ b/kirby/src/Email/Email.php @@ -0,0 +1,187 @@ +, + * Nico Hoffmann + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license MIT +*/ +class Email +{ + use Properties; + + protected $attachments; + protected $body; + protected $bcc; + protected $cc; + protected $from; + protected $replyTo; + 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; + } + + public function body(): 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 isHtml() + { + return $this->body()->html() !== null; + } + + public function isSent(): bool + { + return $this->isSent; + } + + public function replyTo(): string + { + return $this->replyTo; + } + + protected function resolveEmail($email = null, bool $multiple = true) + { + if ($email === null) { + return $multiple === true ? [] : ''; + } + + if (is_array($email) === false) { + $email = [$email]; + } + + foreach ($email as $address) { + if (V::email($address) === false) { + throw new Exception(sprintf('"%s" is not a valid email address', $address)); + } + } + + return $multiple === true ? $email : $email[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 setReplyTo(string $replyTo = null) + { + $this->replyTo = $this->resolveEmail($replyTo, false); + 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..8177a4e --- /dev/null +++ b/kirby/src/Email/PHPMailer.php @@ -0,0 +1,76 @@ +, + * Nico Hoffmann + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license MIT +*/ +class PHPMailer extends Email +{ + public function send(bool $debug = false): bool + { + $mailer = new Mailer(true); + + // set sender's address + $mailer->setFrom($this->from()); + + // optional reply-to address + if ($replyTo = $this->replyTo()) { + $mailer->addReplyTo($replyTo); + } + + // add (multiple) recepient, CC & BCC addresses + foreach ($this->to() as $to) { + $mailer->addAddress($to); + } + foreach ($this->cc() as $cc) { + $mailer->addCC($cc); + } + foreach ($this->bcc() as $bcc) { + $mailer->addBCC($bcc); + } + + $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..a43017d --- /dev/null +++ b/kirby/src/Exception/BadMethodCallException.php @@ -0,0 +1,11 @@ + null]; +} diff --git a/kirby/src/Exception/DuplicateException.php b/kirby/src/Exception/DuplicateException.php new file mode 100755 index 0000000..fa33ec2 --- /dev/null +++ b/kirby/src/Exception/DuplicateException.php @@ -0,0 +1,10 @@ +data = $args['data'] ?? static::$defaultData; + $this->httpCode = $args['httpCode'] ?? static::$defaultHttpCode; + $this->details = $args['details'] ?? static::$defaultDetails; + + 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; + + // Define the Exception key + $key = self::$prefix . '.' . ($args['key'] ?? static::$defaultKey); + + // 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; + } + } + + final public function getData(): array + { + return $this->data; + } + + final public function getDetails(): array + { + return $this->details; + } + + final public function getKey(): string + { + return $this->getCode(); + } + + final public function getHttpCode(): int + { + return $this->httpCode; + } + + final public function isTranslated(): bool + { + return $this->isTranslated; + } + + public function toArray(): array + { + return [ + 'exception' => static::class, + 'message' => $this->getMessage(), + 'key' => $this->getKey(), + 'file' => ltrim($this->getFile(), $_SERVER['DOCUMENT_ROOT'] ?? null), + '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..5aacd63 --- /dev/null +++ b/kirby/src/Exception/InvalidArgumentException.php @@ -0,0 +1,11 @@ + null, 'method' => null]; +} diff --git a/kirby/src/Exception/LogicException.php b/kirby/src/Exception/LogicException.php new file mode 100755 index 0000000..0ed996b --- /dev/null +++ b/kirby/src/Exception/LogicException.php @@ -0,0 +1,10 @@ +validate(); + } + + public function api() + { + if (isset($this->options['api']) === true && is_callable($this->options['api']) === true) { + return $this->options['api']->call($this); + } + } + + 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; + }, + /** + * 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; + } + ] + ]; + } + + 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; + } + + 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() + { + $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..02726dc --- /dev/null +++ b/kirby/src/Form/Fields.php @@ -0,0 +1,52 @@ +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..89a1416 --- /dev/null +++ b/kirby/src/Form/Form.php @@ -0,0 +1,174 @@ +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) { + $props = array_merge($props, [ + 'name' => $props['name'], + 'label' => 'Error in "' . $props['name'] . '" field', + 'theme' => 'negative', + 'text' => $e->getMessage(), + ]); + + $field = new Field('info', $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 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] = (string)$value; + } + } + + return $strings; + } + + public function toArray() + { + $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..53d5dcf --- /dev/null +++ b/kirby/src/Form/Options.php @@ -0,0 +1,172 @@ + '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 + $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..1f4254e --- /dev/null +++ b/kirby/src/Form/OptionsApi.php @@ -0,0 +1,131 @@ +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..63bac5f --- /dev/null +++ b/kirby/src/Form/OptionsQuery.php @@ -0,0 +1,174 @@ +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(), $this->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..56846dc --- /dev/null +++ b/kirby/src/Form/Validations.php @@ -0,0 +1,171 @@ +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 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 $key => $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..5e1cc65 --- /dev/null +++ b/kirby/src/Http/Cookie.php @@ -0,0 +1,206 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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 boolean 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 boolean 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 boolean + */ + 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 boolean 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/Header.php b/kirby/src/Http/Header.php new file mode 100755 index 0000000..0752fbf --- /dev/null +++ b/kirby/src/Http/Header.php @@ -0,0 +1,316 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +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 boolean $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 boolean $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 boolean $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 = isset($codes['_' . $code]) ? $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 boolean $send + * @return string|void + */ + public static function success(bool $send = true) + { + return static::status(200, $send); + } + + /** + * Sends a 201 header + * + * @param boolean $send + * @return string|void + */ + public static function created(bool $send = true) + { + return static::status(201, $send); + } + + /** + * Sends a 202 header + * + * @param boolean $send + * @return string|void + */ + public static function accepted(bool $send = true) + { + return static::status(202, $send); + } + + /** + * Sends a 400 header + * + * @param boolean $send + * @return string|void + */ + public static function error(bool $send = true) + { + return static::status(400, $send); + } + + /** + * Sends a 403 header + * + * @param boolean $send + * @return string|void + */ + public static function forbidden(bool $send = true) + { + return static::status(403, $send); + } + + /** + * Sends a 404 header + * + * @param boolean $send + * @return string|void + */ + public static function notfound(bool $send = true) + { + return static::status(404, $send); + } + + /** + * Sends a 404 header + * + * @param boolean $send + * @return string|void + */ + public static function missing(bool $send = true) + { + return static::status(404, $send); + } + + /** + * Sends a 410 header + * + * @param boolean $send + * @return string|void + */ + public static function gone(bool $send = true) + { + return static::status(410, $send); + } + + /** + * Sends a 500 header + * + * @param boolean $send + * @return string|void + */ + public static function panic(bool $send = true) + { + return static::status(500, $send); + } + + /** + * Sends a 503 header + * + * @param boolean $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 boolean $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..f6c55e9 --- /dev/null +++ b/kirby/src/Http/Idn.php @@ -0,0 +1,27 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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..d72223a --- /dev/null +++ b/kirby/src/Http/Params.php @@ -0,0 +1,145 @@ + 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 boolean $leadingSlash + * @param boolean $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..ea15814 --- /dev/null +++ b/kirby/src/Http/Path.php @@ -0,0 +1,41 @@ +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..cf12769 --- /dev/null +++ b/kirby/src/Http/Query.php @@ -0,0 +1,52 @@ +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..6e63155 --- /dev/null +++ b/kirby/src/Http/Remote.php @@ -0,0 +1,352 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Remote +{ + + /** + * @var array + */ + public static $defaults = [ + 'agent' => 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 integer|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) { + $this->curlopt[CURLOPT_HTTPHEADER] = $this->options['headers']; + } + + // add the user agent + if (empty($this->options['agent']) === false) { + $this->curlopt[CURLOPT_USERAGENT] = $this->options['agent']; + } + + // do some request specific stuff + switch ($action = 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..2f84919 --- /dev/null +++ b/kirby/src/Http/Request.php @@ -0,0 +1,382 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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 object is a tiny + * wrapper around the request method + * name, which will validate and sanitize + * the given name and always return + * its uppercase version. + * + * Examples: + * + * `$request->method()->name()` + * `$request->method()->is('post')` + * + * @var Method + */ + 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() + ]; + } + + /** + * Detects ajax requests + * + * @return boolean + */ + public function ajax(): bool + { + return (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'); + } + + /** + * Returns the Auth object if authentication is set + * + * @return BasicAuth|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 Body + */ + public function body(): Body + { + return $this->body = $this->body ?? new Body(); + } + + /** + * Checks if the request has been made from the command line + * + * @return boolean + */ + 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()); + } + + /** + * 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 Files + */ + public function files(): 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 boolean + */ + 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(); + } + + /** + * Returns the Query object + * + * @return Query + */ + public function query(): Query + { + return $this->query = $this->query ?? new Query(); + } + + /** + * Checks for a valid SSL connection + * + * @return boolean + */ + 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 Uri + */ + public function url(array $props = null): Uri + { + 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..1e5ec35 --- /dev/null +++ b/kirby/src/Http/Request/Auth/BasicAuth.php @@ -0,0 +1,79 @@ +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..f49a8af --- /dev/null +++ b/kirby/src/Http/Request/Auth/BearerAuth.php @@ -0,0 +1,55 @@ +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..f659fc9 --- /dev/null +++ b/kirby/src/Http/Request/Body.php @@ -0,0 +1,129 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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..f828fbe --- /dev/null +++ b/kirby/src/Http/Request/Data.php @@ -0,0 +1,85 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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..f889c8f --- /dev/null +++ b/kirby/src/Http/Request/Files.php @@ -0,0 +1,73 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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..d47ea45 --- /dev/null +++ b/kirby/src/Http/Request/Query.php @@ -0,0 +1,78 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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..08e144a --- /dev/null +++ b/kirby/src/Http/Response.php @@ -0,0 +1,307 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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 integer $code + */ + 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 + * + * @return self + */ + public static function file(string $file) + { + return new static(F::read($file), F::mime($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 integer $code + * @param boolean $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 integer $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..831d03e --- /dev/null +++ b/kirby/src/Http/Route.php @@ -0,0 +1,219 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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 + */ + 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(): Closure + { + return $this->action; + } + + /** + * Returns all parsed arguments + * + * @return array + */ + public function arguments() + { + 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; + } + + /** + * 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..506e7c1 --- /dev/null +++ b/kirby/src/Http/Router.php @@ -0,0 +1,135 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license MIT + */ +class Router +{ + + /** + * 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 + * @return mixed + */ + public function call(string $path = '', string $method = 'GET') + { + return $this + ->find($path, $method) + ->action() + ->call($this->route, ...$this->route->arguments()); + } + + /** + * 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 + * @return Route|null + */ + public function find(string $path, string $method) + { + 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) { + 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 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..9b7d0c0 --- /dev/null +++ b/kirby/src/Http/Server.php @@ -0,0 +1,170 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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 boolean + */ + 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 = intval(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 boolean + */ + 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..25dcd69 --- /dev/null +++ b/kirby/src/Http/Uri.php @@ -0,0 +1,548 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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 boolean + */ + 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() + { + $auth = trim($this->username . ':' . $this->password); + return $auth !== ':' ? $auth : null; + } + + /** + * Returns the base url (scheme + host) + * without trailing slash + * + * @return string + */ + public function base() + { + if (empty($this->host) === true || $this->host === '/') { + return null; + } + + $auth = $this->auth(); + $base = $this->scheme ? $this->scheme . '://' : ''; + + if ($auth !== null) { + $base .= $auth . '@'; + } + + $base .= $this->host; + + if ($this->port !== null && in_array($this->port, [80, 443]) === false) { + $base .= ':' . $this->port; + } + + return $base; + } + + /** + * Clones the Uri object and applies optional + * new props. + * + * @param array $props + * @return self + */ + public function clone(array $props = []): self + { + $clone = clone $this; + + foreach ($props as $key => $value) { + $clone->__set($key, $value); + } + + return $clone; + } + + /** + * @param array $props + * @param boolean $forwarded + * @return self + */ + public static function current(array $props = [], bool $forwarded = false): self + { + if (static::$current !== null) { + return static::$current; + } + + $uri = parse_url('http://getkirby.com' . Server::get('REQUEST_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; + } + + /** + * @return boolean + */ + public function hasFragment(): bool + { + return empty($this->fragment) === false; + } + + /** + * @return boolean + */ + public function hasPath(): bool + { + return $this->path()->isNotEmpty(); + } + + /** + * @return boolean + */ + 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(): self + { + 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): self + { + 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): self + { + $this->host = $host; + return $this; + } + + /** + * @param Params|string|array|null $path + * @return self + */ + public function setParams($params = null): self + { + $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): self + { + $this->password = $password; + return $this; + } + + /** + * @param Path|string|array|null $path + * @return self + */ + public function setPath($path = null): self + { + $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): self + { + 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 string|array|null $query + * @return self + */ + public function setQuery($query = null): self + { + $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): self + { + 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): self + { + $this->slash = $slash; + return $this; + } + + /** + * @param string|null $username + * @return self + */ + public function setUsername(string $username = null): self + { + $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($slash); + + 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(): self + { + 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..93fbc18 --- /dev/null +++ b/kirby/src/Http/Url.php @@ -0,0 +1,282 @@ +$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 (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) + { + // 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 + * + * @return boolean + */ + 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) + { + 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 mixed + */ + 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 mixed + */ + 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 boolean $length + * @param boolean $base + * @param string $rep + * @return string + */ + public static function short($url = null, $length = false, 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 array $options + * @return string + */ + public static function to(string $path = null, array $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 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..2e67b18 --- /dev/null +++ b/kirby/src/Http/Visitor.php @@ -0,0 +1,215 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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 Obj|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 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 Obj|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 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 boolean + */ + public function acceptsMimeType(string $mimeType): bool + { + return Mime::isAccepted($mimeType, $this->acceptedMimeType); + } + + /** + * 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..b4fb997 --- /dev/null +++ b/kirby/src/Image/Camera.php @@ -0,0 +1,94 @@ + +* @link http://getkirby.com +* @copyright Bastian Allgeier +* @license 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..1a3ff2f --- /dev/null +++ b/kirby/src/Image/Darkroom.php @@ -0,0 +1,85 @@ + '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; + } + + 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..87074a3 --- /dev/null +++ b/kirby/src/Image/Darkroom/GdLib.php @@ -0,0 +1,65 @@ +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..2ae33d3 --- /dev/null +++ b/kirby/src/Image/Darkroom/ImageMagick.php @@ -0,0 +1,125 @@ + 'convert', + 'interlace' => false, + ]; + } + + 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 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)); + + exec($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..eb0f13c --- /dev/null +++ b/kirby/src/Image/Dimensions.php @@ -0,0 +1,431 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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 Dimensions + */ + public function crop(int $width, int $height = null): self + { + $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 Dimensions object with recalculated dimensions + */ + public function fit(int $box, bool $force = false): self + { + 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 Dimensions object with recalculated dimensions + */ + public function fitHeight(int $fit = null, bool $force = false): self + { + 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 Dimensions object with recalculated dimensions + */ + protected function fitSize(string $ref, int $fit = null, bool $force = false): self + { + 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 Dimensions object with recalculated dimensions + */ + public function fitWidth(int $fit = null, bool $force = false): self + { + 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 Dimensions + */ + public function fitWidthAndHeight(int $width = null, int $height = null, bool $force = false): self + { + 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): self + { + 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): self + { + // 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 = floatval($attr->width); + $height = floatval($attr->height); + if (($width === 0.0 || $height === 0.0) && empty($attr->viewBox) === false) { + $box = explode(' ', $attr->viewBox); + $width = floatval($box[2] ?? 0); + $height = floatval($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 Dimensions + */ + public function resize(int $width = null, int $height = null, bool $force = false): self + { + 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 + * + * @params 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..99aa169 --- /dev/null +++ b/kirby/src/Image/Exif.php @@ -0,0 +1,297 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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 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 Camera|null + */ + public function camera() + { + if ($this->camera !== null) { + return $this->camera; + } + + return $this->camera = new Camera($this->data); + } + + /** + * Returns the location object + * + * @return 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 boolean|null + */ + public function isColor() + { + return $this->isColor; + } + + /** + * Checks if this is a bw picture + * + * @return boolean|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..7e8bf1d --- /dev/null +++ b/kirby/src/Image/Image.php @@ -0,0 +1,305 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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 Dimensions + */ + public function dimensions(): Dimensions + { + if ($this->dimensions !== null) { + return $this->dimensions; + } + + if (in_array($this->mime(), ['image/jpeg', 'image/png', 'image/gif'])) { + 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 Exif + */ + public function exif(): Exif + { + if ($this->exif !== null) { + return $this->exif; + } + $this->exif = new Exif($this); + return $this->exif; + } + + /** + * Sends an appropriate header for the asset + * + * @param boolean $send + * @return 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 boolean + */ + public function isPortrait(): bool + { + return $this->dimensions()->portrait(); + } + + /** + * Checks if the dimensions of the asset are landscape + * + * @return boolean + */ + public function isLandscape(): bool + { + return $this->dimensions()->landscape(); + } + + /** + * Checks if the dimensions of the asset are square + * + * @return boolean + */ + public function isSquare(): bool + { + return $this->dimensions()->square(); + } + + /** + * Runs a set of validations on the image object + * + * @return bool + */ + public function match(array $rules): bool + { + if (($rules['mime'] ?? null) !== null) { + if (Mime::isAccepted($this->mime(), $rules['mime']) !== true) { + throw new Exception(sprintf('Invalid mime type: %s', $this->mime())); + } + } + + $rules = array_change_key_case($rules); + + $validations = [ + 'maxsize' => ['size', 'max', 'The file is too large'], + 'minsize' => ['size', 'min', 'The file is too small'], + 'maxwidth' => ['width', 'max', 'The width of the image must not exceed %s pixels'], + 'minwidth' => ['width', 'min', 'The width of the image must be at least %s pixels'], + 'maxheight' => ['height', 'max', 'The height of the image must not exceed %s pixels'], + 'minheight' => ['height', 'min', 'The height of the image must be at least %s pixels'], + 'orientation' => ['orientation', 'same', 'The orientation of the image must be "%s"'] + ]; + + foreach ($validations as $key => $arguments) { + if (isset($rules[$key]) === true && $rules[$key] !== null) { + $property = $arguments[0]; + $validator = $arguments[1]; + $message = $arguments[2]; + + if (V::$validator($this->$property(), $rules[$key]) === false) { + throw new Exception(sprintf($message, $rules[$key])); + } + } + } + + 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..f6aff4e --- /dev/null +++ b/kirby/src/Image/Location.php @@ -0,0 +1,137 @@ + +* @link http://getkirby.com +* @copyright Bastian Allgeier +* @license 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 floatval($parts[0]) / floatval($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..4d70e5d --- /dev/null +++ b/kirby/src/Session/AutoSession.php @@ -0,0 +1,175 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license MIT + */ +class AutoSession +{ + protected $sessions; + protected $options; + + protected $createdSession; + + /** + * Creates a new AutoSession instance + * + * @param 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 Session + */ + public function get(array $options = []): Session + { + // 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 Session + */ + public function createManually(array $options = []): Session + { + // only ever allow manual transmission mode + // to prevent overwriting our "auto" session + $options['mode'] = 'manual'; + + return $this->sessions->create($options); + } + + /** + * 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..0daafb4 --- /dev/null +++ b/kirby/src/Session/FileSessionStore.php @@ -0,0 +1,485 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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 boolean 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); + } else { + // the following files are all going to be still valid + break; + } + } + } + + /** + * Cleans up the open locks and file handles + */ + 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..4ad544e --- /dev/null +++ b/kirby/src/Session/Session.php @@ -0,0 +1,768 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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 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 integer 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|integer $expiryTime Optional new expiry timestamp or time string to set + * @return integer 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 integer $duration Optional new duration in seconds to set + * @return integer 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 integer|false $timeout Optional new timeout to set or false to disable timeout + * @return integer|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 boolean $renewable Optional new renewable flag to set + * @return boolean + */ + 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 SessionData + */ + public function data(): SessionData + { + 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; + } + } + + /** + * Returns whether the session token needs to be retransmitted to the client + * Only relevant in header and manual modes + * + * @return boolean + */ + 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|integer $time Timestamp or date string (must be supported by `strtotime()`) + * @param integer $now Timestamp to use as a base for the calculation of relative dates + * @return integer 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, SessionData::class)) { + $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 boolean + */ + 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..70220d0 --- /dev/null +++ b/kirby/src/Session/SessionData.php @@ -0,0 +1,250 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license MIT + */ +class SessionData +{ + protected $session; + protected $data; + + /** + * Creates a new SessionData instance + * + * @param 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 integer $by Increment by which amount? + * @param integer $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 integer $by Decrement by which amount? + * @param integer $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..08b76d4 --- /dev/null +++ b/kirby/src/Session/SessionStore.php @@ -0,0 +1,110 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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 boolean 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..14a1674 --- /dev/null +++ b/kirby/src/Session/Sessions.php @@ -0,0 +1,285 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license MIT + */ +class Sessions +{ + protected $store; + protected $mode; + protected $cookieName; + + protected $cache = []; + + /** + * Creates a new Sessions instance + * + * @param 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 Session + */ + public function create(array $options = []): Session + { + // 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 Session + */ + public function get(string $token, string $mode = null): Session + { + 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 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 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 SessionStore + */ + public function store(): SessionStore + { + 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(); + } + + /** + * 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..90f69f0 --- /dev/null +++ b/kirby/src/Text/KirbyTag.php @@ -0,0 +1,127 @@ +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) { + $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) + { + return null; + } + + public function attr(string $name, $default = null) + { + return $this->$name ?? $default; + } + + public static function factory(...$arguments) + { + return (new static(...$arguments))->render(); + } + + public static function parse(string $string, array $data = [], array $options = []): self + { + // remove the brackets, extract the first attribute (the tag type) + $tag = trim(rtrim(ltrim($string, '('), ')')); + $type = trim(substr($tag, 0, strpos($tag, ':'))); + $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::class) === 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..ac9239e --- /dev/null +++ b/kirby/src/Text/KirbyTags.php @@ -0,0 +1,27 @@ +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..873b4f7 --- /dev/null +++ b/kirby/src/Text/Markdown.php @@ -0,0 +1,76 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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 + * @return string + */ + public function parse(string $text): string + { + if ($this->options['extra'] === true) { + $parser = new ParsedownExtra; + } else { + $parser = new Parsedown; + } + + $parser->setBreaksEnabled($this->options['breaks']); + + // we need the @ here, because parsedown has some notice issues :( + return @$parser->text($text); + } +} diff --git a/kirby/src/Text/SmartyPants.php b/kirby/src/Text/SmartyPants.php new file mode 100755 index 0000000..c4ccbaf --- /dev/null +++ b/kirby/src/Text/SmartyPants.php @@ -0,0 +1,130 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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..c164bec --- /dev/null +++ b/kirby/src/Toolkit/A.php @@ -0,0 +1,585 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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 $array, $key, $default = null) + { + // 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); + + foreach ($keys as $innerKey) { + if (isset($array[$innerKey]) === false) { + return $default; + } + + $array = $array[$innerKey]; + } + + return $array; + } + + return $default; + } + + /** + * @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 boolean $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 boolean 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' + * // ]; + * + * + * @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..c39c839 --- /dev/null +++ b/kirby/src/Toolkit/Collection.php @@ -0,0 +1,1176 @@ +__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 items + * + * @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 collection items + * + * @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 item remover + * + * @param mixed $key the name of the key + */ + public function __unset($key) + { + unset($this->data[$key]); + } + + /** + * Appends an element to the data array + * + * @param mixed $key + * @param mixed $item + * @return Collection + */ + public function append(...$args) + { + if (count($args) === 1) { + $this->data[] = $args[0]; + } elseif (count($args) === 2) { + $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 items per chunk + * @return Collection A new collection with an item for each chunk and + * a sub collection in each chunk + */ + public function chunk(int $size): self + { + // create a multidimensional array that is chunked with the given + // chunk size keep keys of the items + $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(): self + { + return clone $this; + } + + /** + * Getter and setter for the collection 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 items from the collection + * + * @return Collection + */ + public function empty() + { + $collection = clone $this; + $collection->data = []; + + return $collection; + } + + /** + * Adds all items to the collection + * + * @return Collection + */ + public function extend($items): self + { + $collection = clone $this; + return $collection->set($items); + } + + /** + * Filters the collection by a custom + * filter function or an array of filters + * + * @param Closure $filter + * @return self + */ + public function filter($filter): self + { + 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 the collection by one of the predefined + * filter methods. + * + * @param string $field + * @return self + */ + public function filterBy(string $field, ...$args): self + { + $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 collection items 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)) { + $result[$key] = $item; + } + } + + $collection = clone $this; + $collection->data = $result; + return $collection; + } + + /** + * Find a single item by an attribute and its value + * + * @param string $attribute + * @param mixed $value + * @return mixed + */ + public function findBy(string $attribute, $value) + { + foreach ($this->data as $key => $item) { + if ($this->getAttribute($item, $attribute) == $value) { + return $item; + } + } + return null; + } + + /** + * Find a single item by key (id) + * + * @param string $key + * @return mixed + */ + public function findByKey($key) + { + return $this->get($key); + } + + /** + * Returns the first element from the array + * + * @return mixed + */ + public function first() + { + $array = $this->data; + return array_shift($array); + } + + /** + * Returns the array in reverse order + * + * @return Collection + */ + public function flip(): self + { + $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 item + * in the collection. This is useful if items 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 boolean $split + * @return mixed + */ + public function getAttribute($item, string $attribute, $split = false) + { + $value = $this->{'getAttributeFrom' . gettype($item)}($item, $attribute); + + if ($split !== false) { + $value = Str::split($value, $split === true ? ',' : $split); + } + + 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 collection by a given callback + * + * @param Closure $callback + * @return Collection A new collection with an item for each group and a subcollection in each group + */ + public function group(Closure $callback): Collection + { + $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 item to an existing group + $groups[$value]->set($key, $item); + } + } + + return new Collection($groups); + } + + /** + * Groups the collection by a given field + * + * @param string $field + * @param bool $i + * @return Collection A new collection with an item for each group and a subcollection in each group + */ + public function groupBy(string $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; + }); + } + + /** + * Checks if the collection has no items + * + * @return boolean + */ + public function isEmpty(): bool + { + return $this->count() === 0; + } + + /** + * Checks if the number of items in the collection is even + * + * @return boolean + */ + public function isEven(): bool + { + return $this->count() % 2 === 0; + } + + /** + * Checks if the collection has no items + * + * @return boolean + */ + public function isNotEmpty(): bool + { + return $this->count() > 0; + } + + /** + * Checks if the number of items in the collection is odd + * + * @return boolean + */ + public function isOdd(): bool + { + return $this->count() % 2 !== 0; + } + + /** + * Returns the last element from the collection + * + * @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 Collection + */ + public function limit(int $limit): self + { + return $this->slice(0, $limit); + } + + /** + * Map a function to each item in the collection + * + * @param callable $callback + * @return Collection + */ + public function map(callable $callback): self + { + $this->data = array_map($callback, $this->data); + return $this; + } + + /** + * Returns the nth element from the collection + * + * @param integer $n + * @return mixed + */ + public function nth(int $n) + { + return array_values($this->data)[$n] ?? null; + } + + /** + * Returns a Collection without the given element(s) + * + * @param args any number of keys, passed as individual arguments + * @return Collection + */ + public function not(...$keys) + { + $collection = clone $this; + foreach ($keys as $key) { + unset($collection->$key); + } + return $collection; + } + + /** + * Returns a new object starting from the given offset + * + * @param int $offset The index to start from + * @return Collection + */ + public function offset(int $offset): self + { + return $this->slice($offset); + } + + /** + * Add pagination + * + * @return 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 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 Collection + */ + public function prepend(...$args): self + { + if (count($args) === 1) { + array_unshift($this->data, $args[0]); + } elseif (count($args) === 2) { + $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 item to the collection + * + * @param mixed $key string or array + * @param mixed $value + * @return self + */ + public function set($key, $value = null): self + { + if (is_array($key)) { + foreach ($key as $k => $v) { + $this->__set($k, $v); + } + } else { + $this->__set($key, $value); + } + return $this; + } + + /** + * Shuffle all elements in the array + * + * @return Collection + */ + public function shuffle(): self + { + $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 Collection + */ + public function slice(int $offset = 0, int $limit = null): self + { + if ($offset === 0 && $limit === null) { + return $this; + } + + $collection = clone $this; + $collection->data = array_slice($this->data, $offset, $limit); + return $collection; + } + + /** + * Sorts the object by any number of fields + * + * @param $field string|callable Field name or value callback to sort by + * @param $direction string asc or desc + * @param $method int The sort flag, SORT_REGULAR, SORT_NUMERIC etc. + * @return Collection + */ + public function sortBy(): self + { + // 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; + } + + $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 current object into an array + * + * @return array + */ + public function toArray(Closure $map = null): array + { + if ($map !== null) { + return array_map($map, $this->data); + } + + return $this->data; + } + + /** + * Converts the current object into a json string + * + * @return string + */ + public function toJson(): string + { + return json_encode($this->toArray()); + } + + /** + * Convertes the collection 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); + } + + /** + * Alias for $this->not() + * + * @param args any number of keys, passed as individual arguments + * @return 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); + + 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); + + if ($split !== false) { + if (in_array($test, $value) === true) { + unset($collection->data[$key]); + } + } elseif ($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..a88f128 --- /dev/null +++ b/kirby/src/Toolkit/Component.php @@ -0,0 +1,279 @@ +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) { + $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..73ca2c9 --- /dev/null +++ b/kirby/src/Toolkit/Config.php @@ -0,0 +1,21 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +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..60ef26c --- /dev/null +++ b/kirby/src/Toolkit/Controller.php @@ -0,0 +1,66 @@ +function = $function; + } + + public function arguments(array $data = []): array + { + $info = new ReflectionFunction($this->function); + $args = []; + + foreach ($info->getParameters() as $parameter) { + $name = $parameter->getName(); + + if (isset($data[$name]) === false) { + throw new Exception(sprintf('The "%s" parameter is missing', $name)); + } + + $args[] = $data[$name]; + } + + 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..2fb5b71 --- /dev/null +++ b/kirby/src/Toolkit/Dir.php @@ -0,0 +1,401 @@ + $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 boolean 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 boolean + */ + 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..6f03af4 --- /dev/null +++ b/kirby/src/Toolkit/Escape.php @@ -0,0 +1,134 @@ +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..54dc5fb --- /dev/null +++ b/kirby/src/Toolkit/F.php @@ -0,0 +1,738 @@ + [ + 'gz', + 'gzip', + 'tar', + 'tgz', + 'zip', + ], + 'audio' => [ + 'aif', + 'aiff', + 'm4a', + 'midi', + 'mp3', + 'wav', + ], + 'code' => [ + 'css', + 'js', + 'json', + 'java', + 'htm', + 'html', + 'php', + 'rb', + 'py', + 'scss', + 'xml', + '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', + '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 boolean + */ + 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 $file + * @param string $target + * @param boolean $force + * @return boolean + */ + 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 boolean + */ + 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); + } + + /** + * 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 boolean + */ + 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 boolean + */ + public static function isReadable(string $file): bool + { + return is_readable($file); + } + + /** + * Checks if the file is writable + * + * @param string $file + * @return boolean + */ + 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 boolean + */ + 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)); + } + + return $method($source, $link); + } + + /** + * Loads a file and returns the result + * + * @param string $file + * @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 boolean $force Force move if the target file exists + * @return boolean + */ + 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 + unlink($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"', $parent)); + } + + if (substr($realpath, 0, strlen($parent)) !== $parent) { + throw new Exception('The file is not within the parent directory'); + } + } + + return $realpath; + } + + /** + * 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 boolean + */ + 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 + * @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 boolean + */ + 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 boolean $append true: append the content to an exisiting file if available. false: overwrite. + * @return boolean + */ + 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..a9c9a95 --- /dev/null +++ b/kirby/src/Toolkit/Facade.php @@ -0,0 +1,31 @@ +$method(...$args); + } +} diff --git a/kirby/src/Toolkit/File.php b/kirby/src/Toolkit/File.php new file mode 100755 index 0000000..9436490 --- /dev/null +++ b/kirby/src/Toolkit/File.php @@ -0,0 +1,336 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +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 boolean $force + * @return self + */ + public function copy(string $target, bool $force = false): self + { + 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 + * + * @return string + */ + public function dataUri(): string + { + return 'data:' . $this->mime() . ';base64,' . $this->base64(); + } + + /** + * 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 boolean + */ + public function isReadable(): bool + { + return is_readable($this->root) === true; + } + + /** + * Checks if the file is writable + * + * @return boolean + */ + 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): self + { + 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): self + { + $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..0cf27a0 --- /dev/null +++ b/kirby/src/Toolkit/Html.php @@ -0,0 +1,508 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +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 + { + $attr = array_merge(['href' => $href], $attr); + + if (empty($text) === true) { + $text = $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); + } + + /** + * 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']) && isset($value['escape'])) { + $value = $value['escape'] === true ? htmlspecialchars($value['value'], ENT_QUOTES, 'UTF-8') : $value['value']; + } else { + $value = implode(' ', $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 mailto" tag + * + * @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, string $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); + } + + /** + * 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) + { + if ($target === '_blank') { + 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::a('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)) { + $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)) { + $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..ad2ce4d --- /dev/null +++ b/kirby/src/Toolkit/I18n.php @@ -0,0 +1,224 @@ +data = $data; + } + + /** + * Returns the current key from the array + * + * @return string + */ + public function key() + { + return key($this->data); + } + + /** + * Returns an array of all keys in the Iterator + * + * @return array + */ + public function keys(): array + { + return array_keys($this->data); + } + + /** + * Returns the current element of the array + * + * @return mixed + */ + public function current() + { + return current($this->data); + } + + /** + * Moves the cursor to the previous element in the array + * and returns it + * + * @return mixed + */ + public function prev() + { + return prev($this->data); + } + + /** + * Moves the cursor to the next element in the array + * and returns it + * + * @return mixed + */ + public function next() + { + return next($this->data); + } + + /** + * Moves the cusor to the first element of the array + */ + public function rewind() + { + reset($this->data); + } + + /** + * Checks if the current element is valid + * + * @return boolean + */ + public function valid(): bool + { + return $this->current() !== false; + } + + /** + * Counts all elements in the array + * + * @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 if an element is in the collection by key. + * + * @param mixed $key + * @return boolean + */ + 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 boolean + */ + 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..aa4c6ad --- /dev/null +++ b/kirby/src/Toolkit/Mime.php @@ -0,0 +1,285 @@ + '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/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', + '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' => [Mime::class, 'fromSvg'], + ], + 'text/plain' => [ + 'css' => 'text/css', + 'svg' => [Mime::class, '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; + } + + /** + * Undocumented function + * + * @return boolean + */ + public static function isAccepted(string $mime, string $pattern): bool + { + $accepted = Str::accepted($pattern); + + foreach ($accepted as $m) { + if (fnmatch($m['value'], $mime, FNM_PATHNAME) === true) { + return true; + } + } + + return false; + } + + /** + * 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 the mime type of a file + * + * @param string $file + * @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..8142279 --- /dev/null +++ b/kirby/src/Toolkit/Obj.php @@ -0,0 +1,100 @@ + $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 + * + * @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..6d7ecb2 --- /dev/null +++ b/kirby/src/Toolkit/Pagination.php @@ -0,0 +1,410 @@ +page($params['page'] ?? 1); + $this->limit($params['limit'] ?? 20); + $this->total($params['total'] ?? 0); + } + + /** + * Creates a pagination instance for the given + * collection with a flexible argument api + * + * @param 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 and setter for the current page + * + * @param int|null $page + * @return int|Pagination + */ + public function page(int $page = null) + { + if ($page === null) { + if ($this->page > $this->pages()) { + $this->page = $this->lastPage(); + } + + if ($this->page < 1) { + $this->page = $this->firstPage(); + } + + return $this->page; + } + + $this->page = $page; + return $this; + } + + /** + * Getter and setter for the total number of items + * + * @param int|null $total + * @return int|Pagination + */ + public function total(int $total = null) + { + if ($total === null) { + return $this->total; + } + + if ($total < 0) { + throw new Exception('Invalid total number of items: ' . $total); + } + + $this->total = $total; + return $this; + } + + /** + * Getter and setter for the number of items per page + * + * @param int|null $limit + * @return int|Pagination + */ + public function limit(int $limit = null) + { + if ($limit === null) { + return $this->limit; + } + + if ($limit < 1) { + throw new Exception('Invalid pagination limit: ' . $limit); + } + + $this->limit = $limit; + return $this; + } + + /** + * 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 + * + * @return boolean + */ + 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 boolean + */ + public function hasPages(): bool + { + return $this->total() > $this->limit(); + } + + /** + * Checks if there's a previous page + * + * @return boolean + */ + 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 boolean + */ + 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 boolean + */ + public function isFirstPage(): bool + { + return $this->page() === $this->firstPage(); + } + + /** + * Checks if the current page is the last page + * + * @return boolean + */ + public function isLastPage(): bool + { + return $this->page() === $this->lastPage(); + } + + /** + * Creates a range of page numbers for Google-like pagination + * + * @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 + * + * @return int + */ + public function rangeStart(int $range = 5): int + { + return $this->range($range)[0]; + } + + /** + * Returns the last page of the created range + * + * @return int + */ + public function rangeEnd(int $range = 5): int + { + $range = $this->range($range); + return array_pop($range); + } + + /** + * 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..7acf5d3 --- /dev/null +++ b/kirby/src/Toolkit/Properties.php @@ -0,0 +1,143 @@ +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..7b8b8e4 --- /dev/null +++ b/kirby/src/Toolkit/Query.php @@ -0,0 +1,149 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + */ +class Query +{ + + /** + * The query string + * + * @var string + */ + protected $query; + + /** + * Queryable data + * + * @var array + */ + protected $data; + + /** + * Creates a new Query object + * + * @param string $query + * @param array $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; + } + + $parts = $this->parts($this->query); + $data = $this->data; + $value = null; + + while (count($parts)) { + $part = array_shift($parts); + $info = $this->info($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 $token + * @return array + */ + protected function parts(string $token): array + { + $token = trim($token); + $token = preg_replace_callback('!\((.*?)\)!', function ($match) { + return '(' . str_replace('.', '@@@', $match[1]) . ')'; + }, $token); + + $parts = explode('.', $token); + + return $parts; + } + + /** + * Analyzes each part of the query string and + * extracts methods and method arguments. + * + * @param string $token + * @return array + */ + protected function info(string $token): array + { + $args = []; + $method = preg_replace_callback('!\((.*?)\)!', function ($match) use (&$args) { + $args = array_map(function ($arg) { + $arg = trim($arg); + $arg = str_replace('@@@', '.', $arg); + + if (substr($arg, 0, 1) === '"') { + return trim($arg, '"'); + } + + if (substr($arg, 0, 1) === '\'') { + return trim($arg, '\''); + } + + switch ($arg) { + case 'null': + return null; + case 'false': + return false; + case 'true': + return true; + } + + if (is_numeric($arg) === true) { + return (float)$arg; + } + + return $arg; + }, str_getcsv($match[1], ',')); + }, $token); + + return [ + 'method' => $method, + 'args' => $args + ]; + } +} diff --git a/kirby/src/Toolkit/Silo.php b/kirby/src/Toolkit/Silo.php new file mode 100755 index 0000000..7e53af6 --- /dev/null +++ b/kirby/src/Toolkit/Silo.php @@ -0,0 +1,74 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +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 === true) { + 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..659e39c --- /dev/null +++ b/kirby/src/Toolkit/Str.php @@ -0,0 +1,957 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license MIT + */ +class Str +{ + + /** + * Ascii translation table + * + * @var array + */ + protected static $ascii = [ + '/À|Á|Â|Ã|Å|Ǻ|Ā|Ă|Ą|Ǎ|Ä|A/' => 'A', + '/à|á|â|ã|å|ǻ|ā|ă|ą|ǎ|ª|æ|ǽ|ä|a/' => 'a', + '/Б/' => 'B', + '/б/' => 'b', + '/Ç|Ć|Ĉ|Ċ|Č|Ц/' => 'C', + '/ç|ć|ĉ|ċ|č|ц/' => 'c', + '/Ð|Ď|Đ/' => 'Dj', + '/ð|ď|đ/' => 'dj', + '/Д/' => 'D', + '/д/' => 'd', + '/È|É|Ê|Ë|Ē|Ĕ|Ė|Ę|Ě|Е|Ё|Э/' => 'E', + '/è|é|ê|ë|ē|ĕ|ė|ę|ě|е|ё|э/' => 'e', + '/Ф/' => 'F', + '/ƒ|ф/' => 'f', + '/Ĝ|Ğ|Ġ|Ģ|Г/' => 'G', + '/ĝ|ğ|ġ|ģ|г/' => 'g', + '/Ĥ|Ħ|Х/' => 'H', + '/ĥ|ħ|х/' => 'h', + '/Ì|Í|Î|Ï|Ĩ|Ī|Ĭ|Ǐ|Į|İ|И/' => '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 + { + $foreign = static::$ascii; + $string = preg_replace(array_keys($foreign), array_values($foreign), $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 boolean $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; + } + + /** + * 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 boolean + */ + 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 boolean $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, 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) + * @return string The safe string + */ + public static function slug(string $string = null, string $separator = null, string $allowed = null): 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); + + return $string; + } + + /** + * 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; + } + + $string = trim((string)$string, $separator); + $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, 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 + * @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); + } + + /** + * 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..9e3ebda --- /dev/null +++ b/kirby/src/Toolkit/Tpl.php @@ -0,0 +1,53 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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..0e8a0bf --- /dev/null +++ b/kirby/src/Toolkit/V.php @@ -0,0 +1,467 @@ + +* @link http://getkirby.com +* @copyright Bastian Allgeier +* @license 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) { + $value = implode(', ', $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 boolean $fail + * @return boolean|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 boolean + */ + 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 boolean + */ + 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 { + return filter_var($value, FILTER_VALIDATE_EMAIL) !== false; + }, + + /** + * 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 + $regex = '_^(?:(?:https?|ftp)://)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\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]))|(?:(?:[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..732d7ef --- /dev/null +++ b/kirby/src/Toolkit/View.php @@ -0,0 +1,139 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license 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 boolean + */ + 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..2259cf6 --- /dev/null +++ b/kirby/src/Toolkit/Xml.php @@ -0,0 +1,246 @@ + +* @link http://getkirby.com +* @copyright Bastian Allgeier +* @license http://www.opensource.org/licenses/mit-license.php MIT License +*/ +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 boolean $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 boolean $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 + * @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/translations/bg.json b/kirby/translations/bg.json new file mode 100755 index 0000000..68198dc --- /dev/null +++ b/kirby/translations/bg.json @@ -0,0 +1,409 @@ +{ + "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": "Размери", + "discard": "\u041e\u0442\u043c\u0435\u043d\u0438", + "edit": "\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0430\u0439", + + "email": "Email", + "email.placeholder": "mail@example.com", + + "error.access.login": "Invalid login", + "error.access.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.permission": + "Не можете да смените името на \"{filename}\"", + "error.file.duplicate": "Файл с име \"{filename}\" вече съществува", + "error.file.extension.forbidden": + "Файловото разширение \"{extension}\" не е позволено", + "error.file.extension.missing": + "Липсва файлово разширение за файла \"{filename}\"", + "error.file.mime.differs": + "Каченият файл трябва да бъде от същия mime тип \"{mime}\"", + "error.file.mime.forbidden": "The media type \"{mime}\" is not allowed", + "error.file.mime.missing": + "The media type for \"{filename}\" cannot be detected", + "error.file.name.missing": "Името на файла е задължително", + "error.file.notFound": "Файлът \"{filename}\" не може да бъде намерен", + "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.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.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": + "Добавете поне {min} файла в секция \"{section}\"", + "error.section.files.min.singular": + "Добавете поне един файл в секция \"{section}\"", + + "error.section.pages.max.plural": + "Не можете да добавяте повече от {max} страници в секция \"{section}\"", + "error.section.pages.max.singular": + "Не можете да добавяте повече от една страница в секция \"{section}\"", + "error.section.pages.min.plural": + "Добавете поне {min} страници в секция \"{section}\"", + "error.section.pages.min.singular": + "Добавете поне една страница в секция \"{section}\"", + + "error.section.notLoaded": "Секция \"{name}\" не може да бъде заредена", + "error.section.type.invalid": "Типът \"{type}\" на секция не е валиден", + + "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.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.undefined": "\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.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.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.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.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": "Зареждане", + + "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", + "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.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": "Скрит", + + "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.all": "Всички", + "role.empty": "Не съществуват потребители с тази роля", + "role.description.placeholder": "Липсва описание", + + "save": "\u0417\u0430\u043f\u0438\u0448\u0438", + "search": "Търси", + "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.link": "\u0412\u0440\u044a\u0437\u043a\u0430", + "toolbar.button.ol": "Подреден списък", + "toolbar.button.ul": "Списък", + + "translation.author": "Kirby екип", + "translation.direction": "ltr", + "translation.name": "Български", + + "upload": "Прикачи", + "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}?", + + "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/translations/ca.json b/kirby/translations/ca.json new file mode 100755 index 0000000..779613f --- /dev/null +++ b/kirby/translations/ca.json @@ -0,0 +1,409 @@ +{ + "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", + "discard": "Descartar", + "edit": "Editar", + + "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.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.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.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.missing": + "El tipus de suport per a \"{filename}\" no es pot detectar", + "error.file.name.missing": "El nom del fitxer no pot estar buit", + "error.file.notFound": "L'arxiu \"{filename}\" no s'ha trobat", + "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.page.changeSlug.permission": + "No pots canviar la URL d'aquest p\u00e0gina", + "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.notFound": "La p\u00e0gina 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": + "Afegeix com a mínim {min} fitxers a la secció \"{section}\"", + "error.section.files.min.singular": + "Afegeix com a mínim un fitxer a la secció \"{section}\"", + + "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": + "Afegiu com a mínim {min} pàgines a la secció \"{section}\"", + "error.section.pages.min.singular": + "Afegeix com a mínim una pàgina a la secció \"{section}\"", + + "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.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.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.undefined": "L'usuari no s'ha trobat", + "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.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.files.empty": "Encara no hi ha cap fitxer seleccionat", + "field.pages.empty": "Encara no s'ha seleccionat cap pàgina", + "field.structure.delete.confirm": "Est\u00e0s segur d'eliminar aquesta entrada?", + "field.structure.empty": "Encara no hi ha entrades.", + "field.users.empty": "Encara no s'ha seleccionat cap usuari", + + "file.delete.confirm": + "Est\u00e0s segur d'eliminar aquest arxiu?", + + "files": "Arxius", + "files.empty": "Encara no hi ha fitxers", + + "hour": "Hora", + "insert": "Insertar", + "install": "Instal·lar", + + "installation": "Isntal·lació", + "installation.completed": "S'ha instal·lat el panell", + "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 no t\u00e9 perm\u00eds d'escriptura", + "installation.issues.content": + "El directori de contingut i tots els seus arxius i subdirectoris han de tenir perm\u00eds d'escriptura.", + "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": "The /site/sessions folder does not exist or is not writable", + + "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.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": "Texte enlla\u00e7at", + + "loading": "Carregant", + + "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", + "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": + "Est\u00e0s segur d'eliminar aquesta p\u00e0gina?", + "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.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", + + "password": "Contrasenya", + "pixel": "Pixel", + "prev": "Anterior", + "remove": "Eliminar", + "rename": "Canviar el nom", + "replace": "Reempla\u00e7ar", + "retry": "Reintentar", + "revert": "Descartar", + + "role": "Rol", + "role.all": "Tots", + "role.empty": "No hi ha usuaris amb aquest rol", + "role.description.placeholder": "Sense descripció", + + "save": "Desar", + "search": "Cercar", + "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": "Texte 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": "Texte cursiva", + "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", + + "upload": "Carregar", + "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": + "Est\u00e0s segur d'eliminar aquest usuari?", + + "version": "Versi\u00f3 de Kirby", + + "view.account": "La teva compta", + "view.installation": "Isntal\u00b7laci\u00f3", + "view.settings": "Configuració", + "view.site": "Lloc web", + "view.users": "Usuaris", + + "welcome": "Benvinguda", + "year": "Any" +} diff --git a/kirby/translations/da.json b/kirby/translations/da.json new file mode 100755 index 0000000..8eab243 --- /dev/null +++ b/kirby/translations/da.json @@ -0,0 +1,409 @@ +{ + "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", + "discard": "Kass\u00e9r", + "edit": "Rediger", + + "email": "Email", + "email.placeholder": "mail@eksempel.dk", + + "error.access.login": "Ugyldigt log ind", + "error.access.panel": "Du har ikke adgang til 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.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.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.missing": + "Media typen for \"{filename}\" kan ikke bestemmes", + "error.file.name.missing": "Filnavn må ikke være tomt", + "error.file.notFound": "Filen kunne ikke findes", + "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.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.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": + "Tilføj mindst {min} filer til \"{section}\" sektionen", + "error.section.files.min.singular": + "Tilføj mindst en fil til \"{section}\" sektionen", + + "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": + "Tilføj minimum {min} sider til \"{section}\" sektionen", + "error.section.pages.min.singular": + "Tilføj minimum een side til \"{section}\" sektionen", + + "error.section.notLoaded": "Sektionen \"{section}\" kunne ikke indlæses", + "error.section.type.invalid": "Sektionstypen \"{type}\" er ikke gyldig", + + "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.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.undefined": "Brugeren kunne ikke findes", + "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.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.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": "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 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.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", + + "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", + "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.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": "Ulisted", + "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": "Ulisted", + + "password": "Adgangskode", + "pixel": "Pixel", + "prev": "Forrige", + "remove": "Fjern", + "rename": "Omdøb", + "replace": "Erstat", + "retry": "Pr\u00f8v igen", + "revert": "Kass\u00e9r", + + "role": "Rolle", + "role.all": "All", + "role.empty": "Der er ingen bruger med denne rolle", + "role.description.placeholder": "Ingen beskrivelse", + + "save": "Gem", + "search": "Søg", + "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.link": "Link", + "toolbar.button.ol": "Ordnet liste", + "toolbar.button.ul": "Punktliste", + + "translation.author": "Kirby Team", + "translation.direction": "ltr", + "translation.name": "Dansk", + + "upload": "Upload", + "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?", + + "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/translations/de.json b/kirby/translations/de.json new file mode 100755 index 0000000..5c67a8e --- /dev/null +++ b/kirby/translations/de.json @@ -0,0 +1,409 @@ +{ + "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", + "discard": "Verwerfen", + "edit": "Bearbeiten", + + "email": "E-Mail", + "email.placeholder": "mail@beispiel.de", + + "error.access.login": "Ungültige Zugangsdaten", + "error.access.panel": "Du hast keinen Zugang zum Panel", + + "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 größer als 3000 Pixel in der Breite und Höhe ist.", + "error.avatar.mime.forbidden": + "Verbotener Medientyp", + + "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.permission": + "Du kannst 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.mime.differs": + "Die Datei muss den Medientyp \"{mime}\" haben.", + "error.file.mime.forbidden": "Der Medientyp \"{mime}\" ist nicht erlaubt", + "error.file.mime.missing": + "Der Medientyp für \"{filename}\" konnte nicht erkannt werden", + "error.file.name.missing": "Bitte gib einen Dateinamen an", + "error.file.notFound": "Die Datei \"{filename}\" konnte nicht gefunden werden", + "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.page.changeSlug.permission": + "Du kannst 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.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": + "Bitte füge mindestens {min} Dateien zum Bereich \"{section}\" hinzu", + "error.section.files.min.singular": + "Bitte füge mindestens eine Datei zum Bereich \"{section}\" hinzu", + + "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": + "Bitte füge mindestens {min} Seiten zum Bereich \"{section}\" hinzu", + "error.section.pages.min.singular": + "Bitte füge mindestens eine Seite zum Bereich \"{section}\" hinzu", + + "error.section.notLoaded": "Der Bereich \"{name}\" konnte nicht geladen werden", + "error.section.type.invalid": "Der Bereichstyp \"{type}\" ist nicht gültig", + + "error.site.changeTitle.permission": + "Du kannst den Titel der Seite nicht ändern", + "error.site.update.permission": "Du kannst 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.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.undefined": "Der Benutzer wurde nicht gefunden", + "error.user.update.permission": + "Du kannst 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.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.files.empty": "Keine Dateien ausgewählt", + "field.pages.empty": "Keine Seiten ausgwä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 und alle Inhalte müssen beschreibbar sein", + "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": "Der /site/sessions Ordner 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.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": + "Der Lizenzcode steht in deiner Kaufbestätigungsmail. Bitte kopiere ihn 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", + + "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", + "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.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", + + "password": "Passwort", + "pixel": "Pixel", + "prev": "Vorheriger Eintrag", + "remove": "Entfernen", + "rename": "Umbenennen", + "replace": "Ersetzen", + "retry": "Wiederholen", + "revert": "Verwerfen", + + "role": "Rolle", + "role.all": "Alle", + "role.empty": "Keine Benutzer mit dieser Rolle", + "role.description.placeholder": "Keine Beschreibung", + + "save": "Speichern", + "search": "Suchen", + "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.link": "Link", + "toolbar.button.ol": "Geordnete Liste", + "toolbar.button.ul": "Ungeordnete Liste", + + "translation.author": "Kirby Team", + "translation.direction": "ltr", + "translation.name": "Deutsch", + + "upload": "Hochladen", + "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?", + + "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/translations/en.json b/kirby/translations/en.json new file mode 100755 index 0000000..bb96c33 --- /dev/null +++ b/kirby/translations/en.json @@ -0,0 +1,409 @@ +{ + "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", + "discard": "Discard", + "edit": "Edit", + + "email": "Email", + "email.placeholder": "mail@example.com", + + "error.access.login": "Invalid login", + "error.access.panel": "You are not allowed to access 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.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.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.missing": + "The media type for \"{filename}\" cannot be detected", + "error.file.name.missing": "The filename must not be empty", + "error.file.notFound": "The file \"{filename}\" cannot be found", + "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.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.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": + "Add at least {min} files to the \"{section}\" section", + "error.section.files.min.singular": + "Add at least one file to the \"{section}\" section", + + "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": + "Add at least {min} pages to the \"{section}\" section", + "error.section.pages.min.singular": + "Add at least one page to the \"{section}\" section", + + "error.section.notLoaded": "The section \"{name}\" could not be loaded", + "error.section.type.invalid": "The section type \"{type}\" is not valid", + + "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.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.undefined": "The user cannot be found", + "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.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.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.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", + + "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", + "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.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", + + "password": "Password", + "pixel": "Pixel", + "prev": "Previous", + "remove": "Remove", + "rename": "Rename", + "replace": "Replace", + "retry": "Try again", + "revert": "Revert", + + "role": "Role", + "role.all": "All", + "role.empty": "There are no users with this role", + "role.description.placeholder": "No description", + + "save": "Save", + "search": "Search", + "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.link": "Link", + "toolbar.button.ol": "Ordered list", + "toolbar.button.ul": "Bullet list", + + "translation.author": "Kirby Team", + "translation.direction": "ltr", + "translation.name": "English", + + "upload": "Upload", + "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}?", + + "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/translations/fr.json b/kirby/translations/fr.json new file mode 100755 index 0000000..37178ae --- /dev/null +++ b/kirby/translations/fr.json @@ -0,0 +1,409 @@ +{ + "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", + "discard": "Supprimer", + "edit": "Éditer", + + "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.avatar.create.fail": "L’image du profil n’a pas 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.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.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.missing": + "Le type de média de « {filename} » n’a pu être détecté", + "error.file.name.missing": "Veuillez entrer un titre", + "error.file.notFound": "Le fichier « {filename} » n’a pu être trouvé", + "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.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.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": + "Ajoutez au moins {min} fichiers à la section « {section} »", + "error.section.files.min.singular": + "Ajoutez au moins un fichier à la section « {section} »", + + "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": + "Ajoutez au moins {min} pages à la section « {section} »", + "error.section.pages.min.singular": + "Ajoutez au moins une page à la section « {section} »", + + "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.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.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.undefined": "L’utilisateur est introuvable", + "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.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.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": "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": + "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.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", + + "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", + "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.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é", + + "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.all": "Tous", + "role.empty": "Il n’y a aucun utilisateur avec ce rôle", + "role.description.placeholder": "Pas de description", + + "save": "Enregistrer", + "search": "Rechercher", + "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.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", + + "upload": "Transférer", + "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}?", + + "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/translations/hu.json b/kirby/translations/hu.json new file mode 100755 index 0000000..bbae83f --- /dev/null +++ b/kirby/translations/hu.json @@ -0,0 +1,409 @@ +{ + "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", + "discard": "Visszavon\u00e1s", + "edit": "Aloldal szerkeszt\u00e9se", + + "email": "Email", + "email.placeholder": "mail@pelda.hu", + + "error.access.login": "Érvénytelen bejelentkezés", + "error.access.panel": "Nincs jogosultságod megnyitni a panelt", + + "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.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.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.missing": + "A \"{filename}\" fájl típusa nem állapítható meg", + "error.file.name.missing": "A fálj neve nem lehet üres", + "error.file.notFound": "A \"{filename}\" fájl nem található", + "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.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.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": + "Adj hozzá legalább {min} fájlt a(z) \"{section}\" szekcióhoz", + "error.section.files.min.singular": + "Adj hozzá legalább egy fájlt a(z) \"{section}\" szekcióhoz", + + "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": + "Adj hozzá legalább {min} oldalt a(z) \"{section}\" szekcióhoz", + "error.section.pages.min.singular": + "Adj hozzá legalább egy oldalt a(z) \"{section}\" szekcióhoz", + + "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.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.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.undefined": "A felhaszn\u00e1l\u00f3 nem tal\u00e1lhat\u00f3", + "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.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.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": "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": + "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": "The /site/sessions folder does not exist or is not writable", + + "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.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", + + "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ő", + "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.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", + + "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.all": "Összes", + "role.empty": "Nincsenek felhasználók ilyen szerepkörrel", + "role.description.placeholder": "Nincs leírás", + + "save": "Ment\u00e9s", + "search": "Keresés", + "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.link": "Link", + "toolbar.button.ol": "Rendezett lista", + "toolbar.button.ul": "Rendezetlen lista", + + "translation.author": "A Kirby csapata", + "translation.direction": "ltr", + "translation.name": "Magyar", + + "upload": "Feltöltés", + "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}?", + + "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/translations/it.json b/kirby/translations/it.json new file mode 100755 index 0000000..1d5e907 --- /dev/null +++ b/kirby/translations/it.json @@ -0,0 +1,409 @@ +{ + "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", + "discard": "Abbandona", + "edit": "Modifica", + + "email": "Email", + "email.placeholder": "mail@esempio.com", + + "error.access.login": "Login Invalido", + "error.access.panel": "Non ti è permesso accedere al pannello", + + "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.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.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.missing": + "Il MIME type per \"{filename}\" non può essere rilevato", + "error.file.name.missing": "Il nome del file non può essere vuoto", + "error.file.notFound": "Il file non \u00e8 stato trovato", + "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.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.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": + "Aggiungi almeno {min} file alla sezione \"{section}\"", + "error.section.files.min.singular": + "Aggiungi almeno un file alla sezione \"{section}\"", + + "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": + "Aggiungi almeno {min} pagine alla sezione \"{section}\"", + "error.section.pages.min.singular": + "Aggiungi almeno una pagina alla sezione \"{section}\"", + + "error.section.notLoaded": "Non è stato possibile caricare la sezione \"{name}\"", + "error.section.type.invalid": "Il tipo di sezione \"{type}\" non è valido", + + "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.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.undefined": "L'utente non \u00e8 stato trovato", + "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.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.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": "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 non dispone dei permessi di scrittura", + "installation.issues.content": + "La cartella \/content e tutti i file e le cartelle in essa contenuti devono disporre 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 è scrivibile", + "installation.issues.php": "Assicurati di utilizzare PHP 7.1+", + "installation.issues.server": + "Kirby necessita di Apache, Nginx o Caddy", + "installation.issues.sessions": "The /site/sessions folder does not exist or is not writable", + + "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.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", + + "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", + "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.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", + + "password": "Password", + "pixel": "Pixel", + "prev": "Precedente", + "remove": "Rimuovi", + "rename": "Rinomina", + "replace": "Sostituisci", + "retry": "Riprova", + "revert": "Abbandona", + + "role": "Ruolo", + "role.all": "Tutti", + "role.empty": "Non ci sono utenti con questo ruolo", + "role.description.placeholder": "Nessuna descrizione", + + "save": "Salva", + "search": "Cerca", + "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.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", + + "upload": "Carica", + "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?", + + "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/translations/ko.json b/kirby/translations/ko.json new file mode 100755 index 0000000..dcee23f --- /dev/null +++ b/kirby/translations/ko.json @@ -0,0 +1,409 @@ +{ + "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": "크기", + "discard": "무시", + "edit": "\ud3b8\uc9d1", + + "email": "\uc774\uba54\uc77c \uc8fc\uc18c", + "email.placeholder": "mail@example.com", + + "error.access.login": "로그인할 수 없습니다.", + "error.access.panel": "패널에 접근할 권한이 없습니다.", + + "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.permission": + "파일명({filename})을 변경할 권한이 없습니다.", + "error.file.duplicate": "이름이 같은 파일({filename})이 있습니다.", + "error.file.extension.forbidden": + "이 확장자({extension})는 업로드할 수 없습니다.", + "error.file.extension.missing": + "파일({filename})에 확장자가 없습니다.", + "error.file.mime.differs": + "기존 파일과 MIME 형식({mime})이 다릅니다.", + "error.file.mime.forbidden": "The media type \"{mime}\" is not allowed", + "error.file.mime.missing": + "The media type for \"{filename}\" cannot be detected", + "error.file.name.missing": "파일명을 입력하세요.", + "error.file.notFound": "파일({filename})이 없습니다.", + "error.file.type.forbidden": "이 형식({type})의 파일을 업로드할 권한이 없습니다.", + "error.file.undefined": "\ud30c\uc77c\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.", + + "error.form.incomplete": "항목에 오류가 있습니다.", + "error.form.notSaved": "항목을 저장할 수 없습니다.", + + "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.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.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.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.undefined": "\uc0ac\uc6a9\uc790\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.", + "error.user.update.permission": + "사용자({name})의 정보를 변경할 권한이 없습니다.", + + "error.validation.accepted": "확인하세요.", + "error.validation.alpha": "알파벳만 입력할 수 있습니다.", + "error.validation.alphanum": + "알파벳 또는 숫자만 입력할 수 있습니다.", + "error.validation.between": + "{min}과 {max} 사이의 값을 입력하세요.", + "error.validation.boolean": "Please confirm or deny", + "error.validation.contains": + "{needle}에 포함된 값을 입력하세요.", + "error.validation.date": "올바른 날짜를 입력하세요.", + "error.validation.denied": "Please deny", + "error.validation.different": "The value must not be \"{other}\"", + "error.validation.email": "올바른 이메일 주소를 입력하세요.", + "error.validation.endswith": "The value must end with \"{end}\"", + "error.validation.filename": "올바른 파일명을 입력하세요.", + "error.validation.in": "Please enter one of the following: ({in})", + "error.validation.integer": "올바른 정수를 입력하세요.", + "error.validation.ip": "올바른 IP 주소를 입력하세요.", + "error.validation.less": "{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": "올바른 옵션을 선택하세요.", + "error.validation.num": "올바른 숫자를 입력하세요.", + "error.validation.required": "아무거나 입력하세요.", + "error.validation.same": "다음을 입력하세요. {other}", + "error.validation.size": "The size of the value must be \"{size}\"", + "error.validation.startswith": "‘{start}’으(로) 시작해야 합니다.", + "error.validation.time": "올바른 시간을 입력하세요.", + "error.validation.url": "올바른 URL을 입력하세요.", + + "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": "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": + "Apache, Nginx, 또는 Caddy가 필요합니다.", + "installation.issues.sessions": "The /site/sessions folder does not exist or is not writable", + + "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.name": "이름", + "language.updated": "언어가 변경되었습니다.", + + "languages": "언어", + "languages.default": "기본 언어", + "languages.empty": "언어가 없습니다.", + "languages.secondary": "보조 언어", + "languages.secondary.empty": "보조 언어가 없습니다.", + + "license": "라이선스", + "license.buy": "라이선스 구매", + "license.register": "등록", + "license.register.help": + "입력하신 이메일 주소로 라이선스 코드를 전송했습니다. 라이선스 코드를 입력해 등록하세요.", + "license.register.label": "라이선스 코드를 입력하세요.", + "license.register.success": "Kirby를 구입해주셔서 감사합니다.", + "license.unregistered": "Kirby가 등록되지 않았습니다.", + + "link": "\uc77c\ubc18 \ub9c1\ud06c", + "link.text": "\ubb38\uc790", + + "loading": "로딩 중", + + "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": "다음", + "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.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": "비공개", + + "password": "\uc554\ud638", + "pixel": "픽셀", + "prev": "이전", + "remove": "삭제", + "rename": "이름 변경", + "replace": "\uad50\uccb4", + "retry": "\ub2e4\uc2dc \uc2dc\ub3c4", + "revert": "복원", + + "role": "역할", + "role.all": "전체 보기", + "role.empty": "이 역할을 하는 사용자가 없습니다.", + "role.description.placeholder": "설명이 없습니다.", + + "save": "\uc800\uc7a5", + "search": "검색", + "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.link": "링크", + "toolbar.button.ol": "숫자 목록", + "toolbar.button.ul": "기호 목록", + + "translation.author": "Kirby 팀", + "translation.direction": "LTR", + "translation.name": "영어", + + "upload": "업로드", + "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})을 삭제할까요?", + + "version": "버전", + + "view.account": "계정", + "view.installation": "\uc124\uce58", + "view.settings": "설정", + "view.site": "사이트", + "view.users": "\uc0ac\uc6a9\uc790", + + "welcome": "환영합니다.", + "year": "년" +} diff --git a/kirby/translations/nl.json b/kirby/translations/nl.json new file mode 100755 index 0000000..b6f8153 --- /dev/null +++ b/kirby/translations/nl.json @@ -0,0 +1,409 @@ +{ + "add": "Toevoegen", + "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", + "discard": "Annuleren", + "edit": "Wijzig", + + "email": "E-mailadres", + "email.placeholder": "mail@voorbeeld.nl", + + "error.access.login": "Ongeldige login", + "error.access.panel": "Je hebt geen toegang tot 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.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.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.missing": + "Het mediatype voor \"{filename}\" kan niet worden gedecteerd", + "error.file.name.missing": "De bestandsnaam mag niet leeg zijn", + "error.file.notFound": "Het bestand kan niet worden gevonden", + "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.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.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": + "Voeg minmaal {min} bestanden toe aan de zone \"{section}\"", + "error.section.files.min.singular": + "Voeg minimaal 1 bestand toe aan de zone \"{section}\"", + + "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": + "Voeg minimaal {min} pagina's toe aan de zone \"{section}\"", + "error.section.pages.min.singular": + "Voeg minimaal 1 pagina toe aan de zone \"{section}\"", + + "error.section.notLoaded": "De zone \"{name}\" kan niet worden geladen", + "error.section.type.invalid": "Zone-type \"{type}\" is niet geldig", + + "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.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.undefined": "De gebruiker kan niet worden gevonden", + "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.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.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 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": "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": + "De map /site/accounts heeft geen schrijfrechten", + "installation.issues.content": + "De contentmap en alle bestanden hierin moeten schrijfrechten hebben.", + "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.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": "Kirby-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", + + "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", + "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.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", + + "password": "Wachtwoord", + "pixel": "Pixel", + "prev": "Vorige", + "remove": "Verwijder", + "rename": "Hernoem", + "replace": "Vervang", + "retry": "Probeer opnieuw", + "revert": "Annuleren", + + "role": "Rol", + "role.all": "Alle", + "role.empty": "Er zijn geen gebruikers met deze rol", + "role.description.placeholder": "Geen beschrijving", + + "save": "Opslaan", + "search": "Zoeken", + "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.link": "Link", + "toolbar.button.ol": "Genummerde lijst", + "toolbar.button.ul": "Opsomming", + + "translation.author": "Het team van Kirby", + "translation.direction": "ltr", + "translation.name": "Engels", + + "upload": "Upload", + "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?", + + "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/translations/sv_SE.json b/kirby/translations/sv_SE.json new file mode 100755 index 0000000..83a025f --- /dev/null +++ b/kirby/translations/sv_SE.json @@ -0,0 +1,409 @@ +{ + "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", + "discard": "Kassera", + "edit": "Redigera", + + "email": "E-post", + "email.placeholder": "namn@exampel.se", + + "error.access.login": "Ogiltig inloggning", + "error.access.panel": "Du saknar behörighet att nå 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.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.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.missing": + "Mediatypen för \"{filename}\" kan inte detekteras", + "error.file.name.missing": "Filnamnet får inte vara tomt", + "error.file.notFound": "Filen \"{filename}\" kan ej hittas", + "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.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.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": + "Lägg minst {min} filer till sektionen \"{section}\"", + "error.section.files.min.singular": + "Lägg minst en fil till sektionen \"{section}\"", + + "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": + "Lägg till minst {min} sidor till sektionen \"{section}\"", + "error.section.pages.min.singular": + "Lägg till minst en sida till sektionen \"{section}\"", + + "error.section.notLoaded": "Sektionen \"{name}\" kunde inte laddas", + "error.section.type.invalid": "Sektionstypen \"{type}\" är inte giltig", + + "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.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.undefined": "Användaren kan inte hittas", + "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.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.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": "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": + "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": "The /site/sessions folder does not exist or is not writable", + + "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.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", + + "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", + "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.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", + + "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.all": "Alla", + "role.empty": "Det finns inga användare med denna roll", + "role.description.placeholder": "Ingen beskrivning", + + "save": "Spara", + "search": "Sök", + "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.link": "L\u00e4nk", + "toolbar.button.ol": "Sorterad lista", + "toolbar.button.ul": "Punktlista", + + "translation.author": "Kirby-teamet, Ola Christensson", + "translation.direction": "ltr", + "translation.name": "Svenska", + + "upload": "Ladda upp", + "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}?", + + "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/translations/tr.json b/kirby/translations/tr.json new file mode 100755 index 0000000..5976bcd --- /dev/null +++ b/kirby/translations/tr.json @@ -0,0 +1,409 @@ +{ + "add": "Ekle", + "avatar": "Profil resmi", + "back": "Geri", + "cancel": "\u0130ptal", + "change": "De\u011fi\u015ftir", + "close": "Kapat", + "confirm": "Kaydet", + "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", + "discard": "Vazge\u00e7", + "edit": "D\u00fczenle", + + "email": "E-Posta", + "email.placeholder": "eposta@ornek.com", + + "error.access.login": "Geçersiz giriş", + "error.access.panel": "Panele erişim izniniz yok", + + "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": + "\u0130zin verilmeyen dosya tan\u0131mlay\u0131c\u0131s\u0131", + + "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.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.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.missing": + "\"{filename}\" için medya türü tespit edilemiyor", + "error.file.name.missing": "Dosya adı boş bırakılamaz", + "error.file.notFound": "\"{filename}\" dosyası bulunamadı", + "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.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.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üne en az {min} dosya ekleyin", + "error.section.files.min.singular": + "\"{section}\" bölümüne en az bir dosya ekleyin", + + "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üne en az {min} sayfa ekleyin", + "error.section.pages.min.singular": + "\"{section}\" bölümüne en az bir sayfa ekleyin", + + "error.section.notLoaded": "\"{name}\" bölümü yüklenemedi", + "error.section.type.invalid": "\"{type}\" tipi geçerli değil", + + "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.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.undefined": "Kullan\u0131c\u0131 bulunamad\u0131", + "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.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.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": "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 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.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", + + "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", + "open": "Açık", + "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.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ışı", + + "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.all": "Tümü", + "role.empty": "Bu role ait kullanıcı bulunamadı", + "role.description.placeholder": "Açıklama yok", + + "save": "Kaydet", + "search": "Arama", + "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.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", + + "upload": "Yükle", + "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?", + + "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/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..6661723 --- /dev/null +++ b/kirby/vendor/composer/autoload_classmap.php @@ -0,0 +1,244 @@ + $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\\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\\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\\FilePermissions' => $baseDir . '/src/Cms/FilePermissions.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\\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\\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\\Permissions' => $baseDir . '/src/Cms/Permissions.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\\UserRules' => $baseDir . '/src/Cms/UserRules.php', + 'Kirby\\Cms\\Users' => $baseDir . '/src/Cms/Users.php', + 'Kirby\\Cms\\Visitor' => $baseDir . '/src/Cms/Visitor.php', + 'Kirby\\ComposerInstaller\\Installer' => $vendorDir . '/getkirby/composer-installer/src/Installer.php', + 'Kirby\\ComposerInstaller\\Plugin' => $vendorDir . '/getkirby/composer-installer/src/Plugin.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\\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\\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\\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', + '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', + 'Zend\\Escaper\\Escaper' => $vendorDir . '/zendframework/zend-escaper/src/Escaper.php', + 'Zend\\Escaper\\Exception\\ExceptionInterface' => $vendorDir . '/zendframework/zend-escaper/src/Exception/ExceptionInterface.php', + 'Zend\\Escaper\\Exception\\InvalidArgumentException' => $vendorDir . '/zendframework/zend-escaper/src/Exception/InvalidArgumentException.php', + 'Zend\\Escaper\\Exception\\RuntimeException' => $vendorDir . '/zendframework/zend-escaper/src/Exception/RuntimeException.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..1a3ecf4 --- /dev/null +++ b/kirby/vendor/composer/autoload_files.php @@ -0,0 +1,14 @@ + $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', + '04c6c5c2f7095ccf6c481d3e53e1776f' => $vendorDir . '/mustangostang/spyc/Spyc.php', + '87988fc7b1c1f093da22a1a3de972f3a' => $baseDir . '/config/helpers.php', + '428e0a6316e676194f2283f47fbd1fc4' => $baseDir . '/config/aliases.php', + 'd80b806b2b0bfc4457e5f164edcb5232' => $baseDir . '/config/tests.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..c29451b --- /dev/null +++ b/kirby/vendor/composer/autoload_psr4.php @@ -0,0 +1,18 @@ + array($vendorDir . '/zendframework/zend-escaper/src'), + 'Whoops\\' => 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'), + 'Kirby\\ComposerInstaller\\' => array($vendorDir . '/getkirby/composer-installer/src'), + 'Kirby\\' => array($baseDir . '/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..252e720 --- /dev/null +++ b/kirby/vendor/composer/autoload_static.php @@ -0,0 +1,351 @@ + __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', + '04c6c5c2f7095ccf6c481d3e53e1776f' => __DIR__ . '/..' . '/mustangostang/spyc/Spyc.php', + '87988fc7b1c1f093da22a1a3de972f3a' => __DIR__ . '/../..' . '/config/helpers.php', + '428e0a6316e676194f2283f47fbd1fc4' => __DIR__ . '/../..' . '/config/aliases.php', + 'd80b806b2b0bfc4457e5f164edcb5232' => __DIR__ . '/../..' . '/config/tests.php', + ); + + public static $prefixLengthsPsr4 = array ( + 'Z' => + array ( + 'Zend\\Escaper\\' => 13, + ), + 'W' => + array ( + 'Whoops\\' => 7, + ), + 'T' => + array ( + 'TrueBV\\' => 7, + ), + 'S' => + array ( + 'Symfony\\Polyfill\\Mbstring\\' => 26, + ), + 'P' => + array ( + 'Psr\\Log\\' => 8, + 'PHPMailer\\PHPMailer\\' => 20, + ), + 'K' => + array ( + 'Kirby\\ComposerInstaller\\' => 24, + 'Kirby\\' => 6, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'Zend\\Escaper\\' => + array ( + 0 => __DIR__ . '/..' . '/zendframework/zend-escaper/src', + ), + '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', + ), + 'Kirby\\ComposerInstaller\\' => + array ( + 0 => __DIR__ . '/..' . '/getkirby/composer-installer/src', + ), + 'Kirby\\' => + array ( + 0 => __DIR__ . '/../..' . '/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\\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\\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\\FilePermissions' => __DIR__ . '/../..' . '/src/Cms/FilePermissions.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\\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\\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\\Permissions' => __DIR__ . '/../..' . '/src/Cms/Permissions.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\\UserRules' => __DIR__ . '/../..' . '/src/Cms/UserRules.php', + 'Kirby\\Cms\\Users' => __DIR__ . '/../..' . '/src/Cms/Users.php', + 'Kirby\\Cms\\Visitor' => __DIR__ . '/../..' . '/src/Cms/Visitor.php', + 'Kirby\\ComposerInstaller\\Installer' => __DIR__ . '/..' . '/getkirby/composer-installer/src/Installer.php', + 'Kirby\\ComposerInstaller\\Plugin' => __DIR__ . '/..' . '/getkirby/composer-installer/src/Plugin.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\\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\\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\\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', + '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', + 'Zend\\Escaper\\Escaper' => __DIR__ . '/..' . '/zendframework/zend-escaper/src/Escaper.php', + 'Zend\\Escaper\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/zendframework/zend-escaper/src/Exception/ExceptionInterface.php', + 'Zend\\Escaper\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/zendframework/zend-escaper/src/Exception/InvalidArgumentException.php', + 'Zend\\Escaper\\Exception\\RuntimeException' => __DIR__ . '/..' . '/zendframework/zend-escaper/src/Exception/RuntimeException.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/Installer.php b/kirby/vendor/getkirby/composer-installer/src/Installer.php new file mode 100755 index 0000000..c1e4155 --- /dev/null +++ b/kirby/vendor/getkirby/composer-installer/src/Installer.php @@ -0,0 +1,56 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier + * @license MIT + */ +class Installer extends LibraryInstaller +{ + /** + * 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 + $path = $extra['kirby-cms-path'] ?? 'kirby'; + + // 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/Plugin.php b/kirby/vendor/getkirby/composer-installer/src/Plugin.php new file mode 100755 index 0000000..12d6c21 --- /dev/null +++ b/kirby/vendor/getkirby/composer-installer/src/Plugin.php @@ -0,0 +1,29 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier + * @license MIT + */ +class Plugin implements PluginInterface +{ + /** + * Apply plugin modifications to Composer + * + * @param Composer $composer + * @param IOInterface $io + */ + public function activate(Composer $composer, IOInterface $io) + { + $installer = new Installer($io, $composer); + $composer->getInstallationManager()->addInstaller($installer); + } +} diff --git a/kirby/vendor/league/color-extractor/src/League/ColorExtractor/Color.php b/kirby/vendor/league/color-extractor/src/League/ColorExtractor/Color.php new file mode 100755 index 0000000..7b102c1 --- /dev/null +++ b/kirby/vendor/league/color-extractor/src/League/ColorExtractor/Color.php @@ -0,0 +1,51 @@ + $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..4e2c340 --- /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'] = 'Extension missing: '; 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..a3be338 --- /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.6'; + + /** + * 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..9dab992 --- /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.6'; + + /** + * 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..9651e52 --- /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.6'; + + /** + * 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:
+ + + + + diff --git a/site/snippets/gallery.php b/site/snippets/gallery.php new file mode 100755 index 0000000..2a5f9d5 --- /dev/null +++ b/site/snippets/gallery.php @@ -0,0 +1,9 @@ + diff --git a/site/snippets/header.php b/site/snippets/header.php new file mode 100755 index 0000000..14f9259 --- /dev/null +++ b/site/snippets/header.php @@ -0,0 +1,25 @@ + + + + + + + + <?= $site->title() ?> | <?= $page->title() ?> + + + + + + +
+
+ + + +
+ diff --git a/site/snippets/intro.php b/site/snippets/intro.php new file mode 100755 index 0000000..c8e5733 --- /dev/null +++ b/site/snippets/intro.php @@ -0,0 +1,3 @@ +
+

title() ?>

+
diff --git a/site/templates/about.php b/site/templates/about.php new file mode 100755 index 0000000..ffe6c4b --- /dev/null +++ b/site/templates/about.php @@ -0,0 +1,51 @@ + + +
+
+

title() ?>

+
+ +
+ + + +
+ text()->kt() ?> +
+
+
+ + diff --git a/site/templates/album.php b/site/templates/album.php new file mode 100755 index 0000000..73612d2 --- /dev/null +++ b/site/templates/album.php @@ -0,0 +1,39 @@ + + +
+
+ +
+ cover()): ?> +
+ crop(1024, 768) ?> +
+

headline()->or($page->title()) ?>

+
+
+ +
+ +
+ description()->kt() ?> + + tags()->isNotEmpty()): ?> +

tags() ?>

+ +
+ + +
+
+ + diff --git a/site/templates/default.php b/site/templates/default.php new file mode 100755 index 0000000..29e2d3b --- /dev/null +++ b/site/templates/default.php @@ -0,0 +1,12 @@ + + +
+
+

title() ?>

+
+
+ text()->kt() ?> +
+
+ + diff --git a/site/templates/home.php b/site/templates/home.php new file mode 100755 index 0000000..bd103e7 --- /dev/null +++ b/site/templates/home.php @@ -0,0 +1,29 @@ + + +
+
+

title() ?>

+
+ + + +
+ + diff --git a/site/templates/note.php b/site/templates/note.php new file mode 100755 index 0000000..a6b9a26 --- /dev/null +++ b/site/templates/note.php @@ -0,0 +1,19 @@ + + +
+
+
+

title() ?>

+ + tags()->isNotEmpty()) : ?> +

tags() ?>

+ +
+ +
+ text()->kt() ?> +
+
+
+ + diff --git a/site/templates/notes.php b/site/templates/notes.php new file mode 100755 index 0000000..9925ef1 --- /dev/null +++ b/site/templates/notes.php @@ -0,0 +1,23 @@ + + +
+
+

title() ?>

+
+ +
+ children()->listed()->sortBy('date', 'desc') as $note): ?> + + +
+ +
+ + diff --git a/site/templates/photography.php b/site/templates/photography.php new file mode 100755 index 0000000..f980d6e --- /dev/null +++ b/site/templates/photography.php @@ -0,0 +1,24 @@ + + +
+
+

title() ?>

+
+ + +
+ +