From e1a872354b34a7dfed46d4ac04a432999aa1135b Mon Sep 17 00:00:00 2001 From: Stefan Sterz Date: Thu, 3 Dec 2015 14:38:36 +0100 Subject: [PATCH 1/7] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 4d1abad..0815586 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # Sterzy.com This is the code for the http://sterzy.com website. It is based on the kirby cms (http://getkirby.com), which has it's own license agreement ([Kirby End User License Agreement](https://github.com/getkirby/starterkit/blob/master/license.md)). - The rest of the code is under this license: [License](https://svs.ankaa.uberspace.de/sterzy/sterzycom/blob/master/LICENSE "license"), [CC BY 4.0](http://creativecommons.org/licenses/by/4.0/). # Installing From b11bd2998146ef6e2d718239d1cb34d2a8415837 Mon Sep 17 00:00:00 2001 From: Stefan Sterz Date: Thu, 3 Dec 2015 14:39:37 +0100 Subject: [PATCH 2/7] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0815586..4d1abad 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Sterzy.com This is the code for the http://sterzy.com website. It is based on the kirby cms (http://getkirby.com), which has it's own license agreement ([Kirby End User License Agreement](https://github.com/getkirby/starterkit/blob/master/license.md)). + The rest of the code is under this license: [License](https://svs.ankaa.uberspace.de/sterzy/sterzycom/blob/master/LICENSE "license"), [CC BY 4.0](http://creativecommons.org/licenses/by/4.0/). # Installing From 80c59a987646d6ded9d4182e8f24bda7162f5015 Mon Sep 17 00:00:00 2001 From: Stefan Sterz Date: Thu, 26 May 2016 21:02:48 +0200 Subject: [PATCH 3/7] update to version 2.3 and remove submodules --- .gitmodules | 6 - README.md | 1 - index.php | 2 +- kirby | 1 - kirby/bootstrap.php | 69 + kirby/branches/default.php | 18 + kirby/branches/multilang.php | 29 + kirby/branches/multilang/content.php | 41 + kirby/branches/multilang/field.php | 34 + kirby/branches/multilang/file.php | 170 ++ kirby/branches/multilang/language.php | 34 + kirby/branches/multilang/languages.php | 28 + kirby/branches/multilang/page.php | 281 +++ kirby/branches/multilang/site.php | 193 +++ kirby/core/asset.php | 21 + kirby/core/avatar.php | 30 + kirby/core/children.php | 207 +++ kirby/core/content.php | 151 ++ kirby/core/field.php | 55 + kirby/core/file.php | 333 ++++ kirby/core/files.php | 137 ++ kirby/core/kirbytag.php | 173 ++ kirby/core/kirbytext.php | 107 ++ kirby/core/page.php | 1443 ++++++++++++++++ kirby/core/pages.php | 330 ++++ kirby/core/role.php | 102 ++ kirby/core/roles.php | 84 + kirby/core/site.php | 337 ++++ kirby/core/user.php | 349 ++++ kirby/core/users.php | 38 + kirby/extensions/methods.php | 279 +++ kirby/extensions/tags.php | 309 ++++ kirby/helpers.php | 327 ++++ kirby/kirby.php | 785 +++++++++ kirby/kirby/component.php | 27 + kirby/kirby/component/css.php | 52 + kirby/kirby/component/js.php | 51 + kirby/kirby/component/markdown.php | 56 + kirby/kirby/component/response.php | 38 + kirby/kirby/component/smartypants.php | 61 + kirby/kirby/component/snippet.php | 41 + kirby/kirby/component/template.php | 89 + kirby/kirby/component/thumb.php | 206 +++ kirby/kirby/component/tinyurl.php | 54 + kirby/kirby/registry.php | 118 ++ kirby/kirby/registry/blueprint.php | 69 + kirby/kirby/registry/component.php | 39 + kirby/kirby/registry/controller.php | 78 + kirby/kirby/registry/entry.php | 98 ++ kirby/kirby/registry/field.php | 68 + kirby/kirby/registry/hook.php | 30 + kirby/kirby/registry/method.php | 71 + kirby/kirby/registry/model.php | 77 + kirby/kirby/registry/option.php | 42 + kirby/kirby/registry/route.php | 28 + kirby/kirby/registry/snippet.php | 64 + kirby/kirby/registry/tag.php | 42 + kirby/kirby/registry/template.php | 62 + kirby/kirby/registry/widget.php | 67 + kirby/kirby/request.php | 41 + kirby/kirby/request/params.php | 22 + kirby/kirby/request/path.php | 18 + kirby/kirby/request/query.php | 13 + kirby/kirby/roots.php | 99 ++ kirby/kirby/traits/image.php | 207 +++ kirby/kirby/urls.php | 47 + kirby/lib/pageextension.php | 17 + kirby/lib/structure.php | 42 + kirby/readme.md | 18 + kirby/system.php | 21 + kirby/toolkit/bootstrap.php | 97 ++ kirby/toolkit/helpers.php | 353 ++++ kirby/toolkit/lib/a.php | 495 ++++++ kirby/toolkit/lib/bitmask.php | 75 + kirby/toolkit/lib/brick.php | 204 +++ kirby/toolkit/lib/c.php | 17 + kirby/toolkit/lib/cache.php | 59 + kirby/toolkit/lib/cache/driver.php | 190 ++ kirby/toolkit/lib/cache/driver/apc.php | 74 + kirby/toolkit/lib/cache/driver/file.php | 121 ++ kirby/toolkit/lib/cache/driver/memcached.php | 128 ++ kirby/toolkit/lib/cache/driver/mock.php | 74 + kirby/toolkit/lib/cache/driver/session.php | 78 + kirby/toolkit/lib/cache/value.php | 75 + kirby/toolkit/lib/collection.php | 652 +++++++ kirby/toolkit/lib/cookie.php | 168 ++ kirby/toolkit/lib/crypt.php | 86 + kirby/toolkit/lib/data.php | 175 ++ kirby/toolkit/lib/database.php | 496 ++++++ kirby/toolkit/lib/database/query.php | 923 ++++++++++ kirby/toolkit/lib/db.php | 251 +++ kirby/toolkit/lib/detect.php | 261 +++ kirby/toolkit/lib/dimensions.php | 316 ++++ kirby/toolkit/lib/dir.php | 209 +++ kirby/toolkit/lib/email.php | 291 ++++ kirby/toolkit/lib/embed.php | 124 ++ kirby/toolkit/lib/error.php | 26 + kirby/toolkit/lib/errorreporting.php | 94 + kirby/toolkit/lib/escape.php | 266 +++ kirby/toolkit/lib/exif.php | 221 +++ kirby/toolkit/lib/exif/camera.php | 68 + kirby/toolkit/lib/exif/location.php | 118 ++ kirby/toolkit/lib/f.php | 794 +++++++++ kirby/toolkit/lib/folder.php | 406 +++++ kirby/toolkit/lib/header.php | 236 +++ kirby/toolkit/lib/html.php | 255 +++ kirby/toolkit/lib/i.php | 105 ++ kirby/toolkit/lib/l.php | 17 + kirby/toolkit/lib/media.php | 639 +++++++ kirby/toolkit/lib/obj.php | 38 + kirby/toolkit/lib/pagination.php | 417 +++++ kirby/toolkit/lib/password.php | 48 + kirby/toolkit/lib/r.php | 346 ++++ kirby/toolkit/lib/redirect.php | 56 + kirby/toolkit/lib/remote.php | 327 ++++ kirby/toolkit/lib/response.php | 142 ++ kirby/toolkit/lib/router.php | 284 +++ kirby/toolkit/lib/s.php | 291 ++++ kirby/toolkit/lib/server.php | 62 + kirby/toolkit/lib/silo.php | 42 + kirby/toolkit/lib/sql.php | 806 +++++++++ kirby/toolkit/lib/str.php | 701 ++++++++ kirby/toolkit/lib/system.php | 150 ++ kirby/toolkit/lib/thumb.php | 358 ++++ kirby/toolkit/lib/timer.php | 28 + kirby/toolkit/lib/toolkit.php | 20 + kirby/toolkit/lib/tpl.php | 29 + kirby/toolkit/lib/upload.php | 202 +++ kirby/toolkit/lib/url.php | 391 +++++ kirby/toolkit/lib/v.php | 131 ++ kirby/toolkit/lib/visitor.php | 97 ++ kirby/toolkit/lib/xml.php | 144 ++ kirby/toolkit/lib/yaml.php | 57 + kirby/toolkit/readme.md | 32 + .../vendors/abeautifulsite/SimpleImage.php | 1285 ++++++++++++++ .../toolkit/vendors/mimereader/mimereader.php | 776 +++++++++ kirby/toolkit/vendors/yaml/yaml.php | 1156 +++++++++++++ kirby/vendors/parsedown.php | 1528 +++++++++++++++++ kirby/vendors/parsedownextra.php | 526 ++++++ kirby/vendors/smartypants.php | 1094 ++++++++++++ panel | 1 - panel/app/bootstrap.php | 71 + panel/app/config/routes.php | 298 ++++ panel/app/controllers/assets.php | 17 + panel/app/controllers/auth.php | 62 + panel/app/controllers/autocomplete.php | 20 + panel/app/controllers/avatars.php | 49 + panel/app/controllers/dashboard.php | 13 + panel/app/controllers/error.php | 35 + panel/app/controllers/field.php | 82 + panel/app/controllers/files.php | 195 +++ panel/app/controllers/installation.php | 76 + panel/app/controllers/options.php | 40 + panel/app/controllers/pages.php | 218 +++ panel/app/controllers/search.php | 18 + panel/app/controllers/subpages.php | 91 + panel/app/controllers/users.php | 134 ++ panel/app/fields/base/base.php | 191 +++ panel/app/fields/checkbox/checkbox.php | 46 + panel/app/fields/checkboxes/checkboxes.php | 45 + panel/app/fields/date/assets/js/date.js | 50 + panel/app/fields/date/date.php | 63 + panel/app/fields/datetime/datetime.php | 86 + panel/app/fields/email/email.php | 35 + panel/app/fields/filename/filename.php | 18 + .../fields/headline/assets/css/headline.css | 19 + panel/app/fields/headline/headline.php | 29 + panel/app/fields/hidden/hidden.php | 13 + panel/app/fields/image/assets/css/image.css | 19 + panel/app/fields/image/assets/js/image.js | 55 + panel/app/fields/image/image.php | 94 + panel/app/fields/info/info.php | 21 + panel/app/fields/input/input.php | 37 + panel/app/fields/inputlist/inputlist.php | 83 + panel/app/fields/line/line.php | 19 + panel/app/fields/number/number.php | 39 + panel/app/fields/page/page.php | 25 + panel/app/fields/password/password.php | 40 + panel/app/fields/radio/radio.php | 41 + panel/app/fields/select/select.php | 74 + .../fields/structure/assets/css/structure.css | 97 ++ .../fields/structure/assets/js/structure.js | 53 + panel/app/fields/structure/controller.php | 93 + panel/app/fields/structure/forms/add.php | 11 + panel/app/fields/structure/forms/delete.php | 17 + panel/app/fields/structure/forms/update.php | 12 + panel/app/fields/structure/structure.php | 162 ++ panel/app/fields/structure/styles/items.php | 18 + panel/app/fields/structure/styles/table.php | 36 + panel/app/fields/structure/template.php | 20 + panel/app/fields/structure/views/add.php | 3 + panel/app/fields/structure/views/delete.php | 3 + panel/app/fields/structure/views/update.php | 3 + panel/app/fields/tags/tags.php | 52 + panel/app/fields/tel/tel.php | 11 + panel/app/fields/text/assets/css/counter.css | 12 + panel/app/fields/text/assets/js/counter.js | 34 + panel/app/fields/text/text.php | 76 + panel/app/fields/textarea/assets/js/editor.js | 64 + panel/app/fields/textarea/buttons.php | 90 + panel/app/fields/textarea/controller.php | 23 + panel/app/fields/textarea/forms/email.php | 27 + panel/app/fields/textarea/forms/link.php | 28 + panel/app/fields/textarea/textarea.php | 94 + panel/app/fields/textarea/views/email.php | 47 + panel/app/fields/textarea/views/link.php | 53 + panel/app/fields/time/time.php | 69 + panel/app/fields/title/title.php | 40 + panel/app/fields/toggle/toggle.php | 38 + panel/app/fields/url/assets/js/url.js | 38 + panel/app/fields/url/url.php | 30 + panel/app/fields/user/user.php | 22 + panel/app/forms/auth/login.php | 30 + panel/app/forms/avatars/delete.php | 19 + panel/app/forms/editor/email.php | 26 + panel/app/forms/editor/link.php | 26 + panel/app/forms/files/delete.php | 20 + panel/app/forms/files/edit.php | 47 + panel/app/forms/installation/check.php | 35 + panel/app/forms/installation/signup.php | 58 + panel/app/forms/pages/add.php | 44 + panel/app/forms/pages/delete.php | 21 + panel/app/forms/pages/edit.php | 45 + panel/app/forms/pages/template.php | 36 + panel/app/forms/pages/toggle.php | 52 + panel/app/forms/pages/url.php | 34 + panel/app/forms/users/delete.php | 22 + panel/app/forms/users/user.php | 120 ++ panel/app/helpers.php | 75 + panel/app/layouts/app.php | 31 + panel/app/layouts/base.php | 21 + panel/app/layouts/fatal.php | 32 + panel/app/snippets/breadcrumb.php | 19 + panel/app/snippets/languages.php | 21 + panel/app/snippets/menu.php | 28 + panel/app/snippets/meta.php | 4 + panel/app/snippets/pages/sidebar.php | 19 + panel/app/snippets/pages/sidebar/file.php | 6 + panel/app/snippets/pages/sidebar/files.php | 27 + panel/app/snippets/pages/sidebar/subpage.php | 7 + panel/app/snippets/pages/sidebar/subpages.php | 42 + panel/app/snippets/pagination.php | 14 + panel/app/snippets/search.php | 10 + panel/app/snippets/subpages/subpage.php | 29 + panel/app/snippets/template.php | 27 + panel/app/snippets/uploader.php | 20 + panel/app/src/panel.php | 559 ++++++ panel/app/src/panel/autocomplete.php | 119 ++ panel/app/src/panel/collections/children.php | 129 ++ panel/app/src/panel/collections/files.php | 51 + panel/app/src/panel/collections/users.php | 39 + panel/app/src/panel/controllers/base.php | 121 ++ panel/app/src/panel/controllers/field.php | 47 + panel/app/src/panel/form.php | 340 ++++ panel/app/src/panel/form/fieldoptions.php | 256 +++ panel/app/src/panel/form/plugins.php | 113 ++ panel/app/src/panel/installer.php | 78 + panel/app/src/panel/layout.php | 14 + panel/app/src/panel/login.php | 246 +++ panel/app/src/panel/models/file.php | 250 +++ panel/app/src/panel/models/file/menu.php | 69 + panel/app/src/panel/models/page.php | 644 +++++++ panel/app/src/panel/models/page/addbutton.php | 21 + panel/app/src/panel/models/page/blueprint.php | 126 ++ .../src/panel/models/page/blueprint/field.php | 115 ++ .../panel/models/page/blueprint/fields.php | 48 + .../src/panel/models/page/blueprint/files.php | 57 + .../panel/models/page/blueprint/options.php | 46 + .../src/panel/models/page/blueprint/pages.php | 96 ++ panel/app/src/panel/models/page/changes.php | 121 ++ panel/app/src/panel/models/page/menu.php | 166 ++ panel/app/src/panel/models/page/sidebar.php | 78 + panel/app/src/panel/models/page/sorter.php | 123 ++ panel/app/src/panel/models/page/uploader.php | 188 ++ panel/app/src/panel/models/site.php | 222 +++ panel/app/src/panel/models/user.php | 151 ++ panel/app/src/panel/models/user/avatar.php | 78 + panel/app/src/panel/models/user/blueprint.php | 59 + panel/app/src/panel/models/user/history.php | 94 + panel/app/src/panel/roots.php | 34 + panel/app/src/panel/search.php | 109 ++ panel/app/src/panel/snippet.php | 14 + panel/app/src/panel/structure.php | 181 ++ panel/app/src/panel/structure/store.php | 185 ++ panel/app/src/panel/topbar.php | 117 ++ panel/app/src/panel/translation.php | 80 + panel/app/src/panel/upload.php | 22 + panel/app/src/panel/urls.php | 35 + panel/app/src/panel/view.php | 40 + panel/app/src/panel/widgets.php | 92 + panel/app/topbars/error.php | 5 + panel/app/topbars/user.php | 13 + panel/app/translations/ar/core.json | 311 ++++ panel/app/translations/ar/package.json | 6 + panel/app/translations/bg/core.json | 311 ++++ panel/app/translations/bg/package.json | 4 + panel/app/translations/ca/core.json | 311 ++++ panel/app/translations/ca/package.json | 4 + panel/app/translations/cs/core.json | 311 ++++ panel/app/translations/cs/package.json | 4 + panel/app/translations/da/core.json | 311 ++++ panel/app/translations/da/package.json | 4 + panel/app/translations/de/core.json | 311 ++++ panel/app/translations/de/package.json | 4 + panel/app/translations/en/core.json | 311 ++++ panel/app/translations/en/package.json | 4 + panel/app/translations/es_419/core.json | 311 ++++ panel/app/translations/es_419/package.json | 4 + panel/app/translations/es_ES/core.json | 311 ++++ panel/app/translations/es_ES/package.json | 4 + panel/app/translations/fa/core.json | 311 ++++ panel/app/translations/fa/package.json | 4 + panel/app/translations/fi/core.json | 311 ++++ panel/app/translations/fi/package.json | 4 + panel/app/translations/fr/core.json | 311 ++++ panel/app/translations/fr/package.json | 4 + panel/app/translations/hu/core.json | 311 ++++ panel/app/translations/hu/package.json | 4 + panel/app/translations/id/core.json | 311 ++++ panel/app/translations/id/package.json | 4 + panel/app/translations/it/core.json | 311 ++++ panel/app/translations/it/package.json | 4 + panel/app/translations/ja/core.json | 311 ++++ panel/app/translations/ja/package.json | 4 + panel/app/translations/nb/core.json | 311 ++++ panel/app/translations/nb/package.json | 4 + panel/app/translations/nl/core.json | 311 ++++ panel/app/translations/nl/package.json | 4 + panel/app/translations/pl/core.json | 311 ++++ panel/app/translations/pl/package.json | 4 + panel/app/translations/pt_BR/core.json | 311 ++++ panel/app/translations/pt_BR/package.json | 4 + panel/app/translations/pt_PT/core.json | 311 ++++ panel/app/translations/pt_PT/package.json | 4 + panel/app/translations/ro/core.json | 311 ++++ panel/app/translations/ro/package.json | 4 + panel/app/translations/ru/core.json | 311 ++++ panel/app/translations/ru/package.json | 4 + panel/app/translations/sv_SE/core.json | 311 ++++ panel/app/translations/sv_SE/package.json | 4 + panel/app/translations/tr/core.json | 311 ++++ panel/app/translations/tr/package.json | 4 + panel/app/translations/zh_CN/core.json | 311 ++++ panel/app/translations/zh_CN/package.json | 4 + panel/app/translations/zh_TW/core.json | 311 ++++ panel/app/translations/zh_TW/package.json | 4 + panel/app/views/auth/block.php | 16 + panel/app/views/auth/error.php | 16 + panel/app/views/auth/login.php | 19 + panel/app/views/avatars/delete.php | 3 + panel/app/views/dashboard/index.php | 49 + panel/app/views/error/index.php | 4 + panel/app/views/error/modal.php | 18 + panel/app/views/files/delete.php | 3 + panel/app/views/files/edit.php | 88 + panel/app/views/files/index.php | 104 ++ panel/app/views/installation/index.php | 23 + panel/app/views/options/index.php | 62 + panel/app/views/pages/add.php | 25 + panel/app/views/pages/delete.php | 3 + panel/app/views/pages/edit.php | 31 + panel/app/views/pages/template.php | 21 + panel/app/views/pages/toggle.php | 3 + panel/app/views/pages/url.php | 33 + panel/app/views/search/results.php | 35 + panel/app/views/subpages/index.php | 157 ++ panel/app/views/users/delete.php | 3 + panel/app/views/users/edit.php | 108 ++ panel/app/views/users/index.php | 60 + panel/app/widgets/account/account.html.php | 10 + panel/app/widgets/account/account.php | 22 + panel/app/widgets/history/history.html.php | 18 + panel/app/widgets/history/history.php | 13 + panel/app/widgets/license/license.html.php | 15 + panel/app/widgets/license/license.php | 25 + panel/app/widgets/pages/pages.html.php | 5 + panel/app/widgets/pages/pages.php | 36 + panel/app/widgets/site/site.html.php | 8 + panel/app/widgets/site/site.php | 12 + panel/assets/css/form.min.css | 1 + panel/assets/css/panel.min.css | 4 + panel/assets/fonts/fontawesome-webfont.woff | Bin 0 -> 83588 bytes panel/assets/fonts/fontawesome-webfont.woff2 | Bin 0 -> 66624 bytes .../fonts/sourcesanspro-400-italic.woff | Bin 0 -> 60836 bytes .../fonts/sourcesanspro-400-italic.woff2 | Bin 0 -> 36016 bytes panel/assets/fonts/sourcesanspro-400.woff | Bin 0 -> 75420 bytes panel/assets/fonts/sourcesanspro-400.woff2 | Bin 0 -> 86844 bytes panel/assets/fonts/sourcesanspro-600.woff | Bin 0 -> 74996 bytes panel/assets/fonts/sourcesanspro-600.woff2 | Bin 0 -> 86196 bytes panel/assets/images/avatar.png | Bin 0 -> 324 bytes panel/assets/images/hint.arrows.png | Bin 0 -> 549 bytes panel/assets/images/loader.black.gif | Bin 0 -> 718 bytes panel/assets/images/loader.white.gif | Bin 0 -> 718 bytes panel/assets/images/pattern.png | Bin 0 -> 116 bytes panel/assets/images/placeholder.png | Bin 0 -> 98 bytes panel/assets/js/dist/app.min.js | 1 + panel/assets/js/dist/form.min.js | 1 + panel/assets/js/dist/panel.min.js | 8 + panel/index.php | 55 + panel/readme.md | 16 + site/fields/markdown | 2 +- site/templates/home.php | 2 +- 402 files changed, 49907 insertions(+), 12 deletions(-) delete mode 160000 kirby create mode 100644 kirby/bootstrap.php create mode 100644 kirby/branches/default.php create mode 100644 kirby/branches/multilang.php create mode 100644 kirby/branches/multilang/content.php create mode 100644 kirby/branches/multilang/field.php create mode 100644 kirby/branches/multilang/file.php create mode 100644 kirby/branches/multilang/language.php create mode 100644 kirby/branches/multilang/languages.php create mode 100644 kirby/branches/multilang/page.php create mode 100644 kirby/branches/multilang/site.php create mode 100644 kirby/core/asset.php create mode 100644 kirby/core/avatar.php create mode 100644 kirby/core/children.php create mode 100644 kirby/core/content.php create mode 100644 kirby/core/field.php create mode 100644 kirby/core/file.php create mode 100644 kirby/core/files.php create mode 100644 kirby/core/kirbytag.php create mode 100644 kirby/core/kirbytext.php create mode 100644 kirby/core/page.php create mode 100644 kirby/core/pages.php create mode 100644 kirby/core/role.php create mode 100644 kirby/core/roles.php create mode 100644 kirby/core/site.php create mode 100644 kirby/core/user.php create mode 100644 kirby/core/users.php create mode 100644 kirby/extensions/methods.php create mode 100644 kirby/extensions/tags.php create mode 100644 kirby/helpers.php create mode 100644 kirby/kirby.php create mode 100644 kirby/kirby/component.php create mode 100644 kirby/kirby/component/css.php create mode 100644 kirby/kirby/component/js.php create mode 100644 kirby/kirby/component/markdown.php create mode 100644 kirby/kirby/component/response.php create mode 100644 kirby/kirby/component/smartypants.php create mode 100644 kirby/kirby/component/snippet.php create mode 100644 kirby/kirby/component/template.php create mode 100644 kirby/kirby/component/thumb.php create mode 100644 kirby/kirby/component/tinyurl.php create mode 100644 kirby/kirby/registry.php create mode 100644 kirby/kirby/registry/blueprint.php create mode 100644 kirby/kirby/registry/component.php create mode 100644 kirby/kirby/registry/controller.php create mode 100644 kirby/kirby/registry/entry.php create mode 100644 kirby/kirby/registry/field.php create mode 100644 kirby/kirby/registry/hook.php create mode 100644 kirby/kirby/registry/method.php create mode 100644 kirby/kirby/registry/model.php create mode 100644 kirby/kirby/registry/option.php create mode 100644 kirby/kirby/registry/route.php create mode 100644 kirby/kirby/registry/snippet.php create mode 100644 kirby/kirby/registry/tag.php create mode 100644 kirby/kirby/registry/template.php create mode 100644 kirby/kirby/registry/widget.php create mode 100644 kirby/kirby/request.php create mode 100644 kirby/kirby/request/params.php create mode 100644 kirby/kirby/request/path.php create mode 100644 kirby/kirby/request/query.php create mode 100644 kirby/kirby/roots.php create mode 100644 kirby/kirby/traits/image.php create mode 100644 kirby/kirby/urls.php create mode 100644 kirby/lib/pageextension.php create mode 100644 kirby/lib/structure.php create mode 100644 kirby/readme.md create mode 100644 kirby/system.php create mode 100644 kirby/toolkit/bootstrap.php create mode 100644 kirby/toolkit/helpers.php create mode 100644 kirby/toolkit/lib/a.php create mode 100644 kirby/toolkit/lib/bitmask.php create mode 100644 kirby/toolkit/lib/brick.php create mode 100644 kirby/toolkit/lib/c.php create mode 100644 kirby/toolkit/lib/cache.php create mode 100755 kirby/toolkit/lib/cache/driver.php create mode 100644 kirby/toolkit/lib/cache/driver/apc.php create mode 100644 kirby/toolkit/lib/cache/driver/file.php create mode 100644 kirby/toolkit/lib/cache/driver/memcached.php create mode 100644 kirby/toolkit/lib/cache/driver/mock.php create mode 100644 kirby/toolkit/lib/cache/driver/session.php create mode 100644 kirby/toolkit/lib/cache/value.php create mode 100644 kirby/toolkit/lib/collection.php create mode 100644 kirby/toolkit/lib/cookie.php create mode 100644 kirby/toolkit/lib/crypt.php create mode 100644 kirby/toolkit/lib/data.php create mode 100644 kirby/toolkit/lib/database.php create mode 100644 kirby/toolkit/lib/database/query.php create mode 100644 kirby/toolkit/lib/db.php create mode 100644 kirby/toolkit/lib/detect.php create mode 100644 kirby/toolkit/lib/dimensions.php create mode 100644 kirby/toolkit/lib/dir.php create mode 100644 kirby/toolkit/lib/email.php create mode 100644 kirby/toolkit/lib/embed.php create mode 100644 kirby/toolkit/lib/error.php create mode 100644 kirby/toolkit/lib/errorreporting.php create mode 100644 kirby/toolkit/lib/escape.php create mode 100644 kirby/toolkit/lib/exif.php create mode 100644 kirby/toolkit/lib/exif/camera.php create mode 100644 kirby/toolkit/lib/exif/location.php create mode 100644 kirby/toolkit/lib/f.php create mode 100644 kirby/toolkit/lib/folder.php create mode 100644 kirby/toolkit/lib/header.php create mode 100644 kirby/toolkit/lib/html.php create mode 100644 kirby/toolkit/lib/i.php create mode 100644 kirby/toolkit/lib/l.php create mode 100644 kirby/toolkit/lib/media.php create mode 100644 kirby/toolkit/lib/obj.php create mode 100644 kirby/toolkit/lib/pagination.php create mode 100644 kirby/toolkit/lib/password.php create mode 100644 kirby/toolkit/lib/r.php create mode 100644 kirby/toolkit/lib/redirect.php create mode 100644 kirby/toolkit/lib/remote.php create mode 100644 kirby/toolkit/lib/response.php create mode 100644 kirby/toolkit/lib/router.php create mode 100644 kirby/toolkit/lib/s.php create mode 100644 kirby/toolkit/lib/server.php create mode 100644 kirby/toolkit/lib/silo.php create mode 100644 kirby/toolkit/lib/sql.php create mode 100644 kirby/toolkit/lib/str.php create mode 100644 kirby/toolkit/lib/system.php create mode 100644 kirby/toolkit/lib/thumb.php create mode 100644 kirby/toolkit/lib/timer.php create mode 100644 kirby/toolkit/lib/toolkit.php create mode 100644 kirby/toolkit/lib/tpl.php create mode 100644 kirby/toolkit/lib/upload.php create mode 100644 kirby/toolkit/lib/url.php create mode 100644 kirby/toolkit/lib/v.php create mode 100644 kirby/toolkit/lib/visitor.php create mode 100644 kirby/toolkit/lib/xml.php create mode 100644 kirby/toolkit/lib/yaml.php create mode 100644 kirby/toolkit/readme.md create mode 100755 kirby/toolkit/vendors/abeautifulsite/SimpleImage.php create mode 100644 kirby/toolkit/vendors/mimereader/mimereader.php create mode 100644 kirby/toolkit/vendors/yaml/yaml.php create mode 100644 kirby/vendors/parsedown.php create mode 100644 kirby/vendors/parsedownextra.php create mode 100755 kirby/vendors/smartypants.php delete mode 160000 panel create mode 100644 panel/app/bootstrap.php create mode 100644 panel/app/config/routes.php create mode 100644 panel/app/controllers/assets.php create mode 100644 panel/app/controllers/auth.php create mode 100644 panel/app/controllers/autocomplete.php create mode 100644 panel/app/controllers/avatars.php create mode 100644 panel/app/controllers/dashboard.php create mode 100644 panel/app/controllers/error.php create mode 100644 panel/app/controllers/field.php create mode 100644 panel/app/controllers/files.php create mode 100644 panel/app/controllers/installation.php create mode 100644 panel/app/controllers/options.php create mode 100644 panel/app/controllers/pages.php create mode 100644 panel/app/controllers/search.php create mode 100644 panel/app/controllers/subpages.php create mode 100644 panel/app/controllers/users.php create mode 100644 panel/app/fields/base/base.php create mode 100644 panel/app/fields/checkbox/checkbox.php create mode 100644 panel/app/fields/checkboxes/checkboxes.php create mode 100755 panel/app/fields/date/assets/js/date.js create mode 100644 panel/app/fields/date/date.php create mode 100644 panel/app/fields/datetime/datetime.php create mode 100644 panel/app/fields/email/email.php create mode 100644 panel/app/fields/filename/filename.php create mode 100644 panel/app/fields/headline/assets/css/headline.css create mode 100644 panel/app/fields/headline/headline.php create mode 100644 panel/app/fields/hidden/hidden.php create mode 100644 panel/app/fields/image/assets/css/image.css create mode 100644 panel/app/fields/image/assets/js/image.js create mode 100644 panel/app/fields/image/image.php create mode 100644 panel/app/fields/info/info.php create mode 100644 panel/app/fields/input/input.php create mode 100644 panel/app/fields/inputlist/inputlist.php create mode 100644 panel/app/fields/line/line.php create mode 100644 panel/app/fields/number/number.php create mode 100644 panel/app/fields/page/page.php create mode 100644 panel/app/fields/password/password.php create mode 100644 panel/app/fields/radio/radio.php create mode 100644 panel/app/fields/select/select.php create mode 100644 panel/app/fields/structure/assets/css/structure.css create mode 100644 panel/app/fields/structure/assets/js/structure.js create mode 100644 panel/app/fields/structure/controller.php create mode 100644 panel/app/fields/structure/forms/add.php create mode 100644 panel/app/fields/structure/forms/delete.php create mode 100644 panel/app/fields/structure/forms/update.php create mode 100644 panel/app/fields/structure/structure.php create mode 100644 panel/app/fields/structure/styles/items.php create mode 100644 panel/app/fields/structure/styles/table.php create mode 100644 panel/app/fields/structure/template.php create mode 100644 panel/app/fields/structure/views/add.php create mode 100644 panel/app/fields/structure/views/delete.php create mode 100644 panel/app/fields/structure/views/update.php create mode 100644 panel/app/fields/tags/tags.php create mode 100644 panel/app/fields/tel/tel.php create mode 100644 panel/app/fields/text/assets/css/counter.css create mode 100644 panel/app/fields/text/assets/js/counter.js create mode 100644 panel/app/fields/text/text.php create mode 100644 panel/app/fields/textarea/assets/js/editor.js create mode 100644 panel/app/fields/textarea/buttons.php create mode 100644 panel/app/fields/textarea/controller.php create mode 100644 panel/app/fields/textarea/forms/email.php create mode 100644 panel/app/fields/textarea/forms/link.php create mode 100644 panel/app/fields/textarea/textarea.php create mode 100644 panel/app/fields/textarea/views/email.php create mode 100644 panel/app/fields/textarea/views/link.php create mode 100644 panel/app/fields/time/time.php create mode 100644 panel/app/fields/title/title.php create mode 100644 panel/app/fields/toggle/toggle.php create mode 100644 panel/app/fields/url/assets/js/url.js create mode 100644 panel/app/fields/url/url.php create mode 100644 panel/app/fields/user/user.php create mode 100644 panel/app/forms/auth/login.php create mode 100644 panel/app/forms/avatars/delete.php create mode 100644 panel/app/forms/editor/email.php create mode 100644 panel/app/forms/editor/link.php create mode 100644 panel/app/forms/files/delete.php create mode 100644 panel/app/forms/files/edit.php create mode 100644 panel/app/forms/installation/check.php create mode 100644 panel/app/forms/installation/signup.php create mode 100644 panel/app/forms/pages/add.php create mode 100644 panel/app/forms/pages/delete.php create mode 100644 panel/app/forms/pages/edit.php create mode 100644 panel/app/forms/pages/template.php create mode 100644 panel/app/forms/pages/toggle.php create mode 100644 panel/app/forms/pages/url.php create mode 100644 panel/app/forms/users/delete.php create mode 100644 panel/app/forms/users/user.php create mode 100644 panel/app/helpers.php create mode 100644 panel/app/layouts/app.php create mode 100644 panel/app/layouts/base.php create mode 100644 panel/app/layouts/fatal.php create mode 100644 panel/app/snippets/breadcrumb.php create mode 100644 panel/app/snippets/languages.php create mode 100644 panel/app/snippets/menu.php create mode 100644 panel/app/snippets/meta.php create mode 100644 panel/app/snippets/pages/sidebar.php create mode 100644 panel/app/snippets/pages/sidebar/file.php create mode 100644 panel/app/snippets/pages/sidebar/files.php create mode 100644 panel/app/snippets/pages/sidebar/subpage.php create mode 100644 panel/app/snippets/pages/sidebar/subpages.php create mode 100644 panel/app/snippets/pagination.php create mode 100644 panel/app/snippets/search.php create mode 100644 panel/app/snippets/subpages/subpage.php create mode 100644 panel/app/snippets/template.php create mode 100644 panel/app/snippets/uploader.php create mode 100644 panel/app/src/panel.php create mode 100644 panel/app/src/panel/autocomplete.php create mode 100644 panel/app/src/panel/collections/children.php create mode 100644 panel/app/src/panel/collections/files.php create mode 100644 panel/app/src/panel/collections/users.php create mode 100644 panel/app/src/panel/controllers/base.php create mode 100644 panel/app/src/panel/controllers/field.php create mode 100644 panel/app/src/panel/form.php create mode 100644 panel/app/src/panel/form/fieldoptions.php create mode 100644 panel/app/src/panel/form/plugins.php create mode 100644 panel/app/src/panel/installer.php create mode 100644 panel/app/src/panel/layout.php create mode 100644 panel/app/src/panel/login.php create mode 100644 panel/app/src/panel/models/file.php create mode 100644 panel/app/src/panel/models/file/menu.php create mode 100644 panel/app/src/panel/models/page.php create mode 100644 panel/app/src/panel/models/page/addbutton.php create mode 100644 panel/app/src/panel/models/page/blueprint.php create mode 100644 panel/app/src/panel/models/page/blueprint/field.php create mode 100644 panel/app/src/panel/models/page/blueprint/fields.php create mode 100644 panel/app/src/panel/models/page/blueprint/files.php create mode 100644 panel/app/src/panel/models/page/blueprint/options.php create mode 100644 panel/app/src/panel/models/page/blueprint/pages.php create mode 100644 panel/app/src/panel/models/page/changes.php create mode 100644 panel/app/src/panel/models/page/menu.php create mode 100644 panel/app/src/panel/models/page/sidebar.php create mode 100644 panel/app/src/panel/models/page/sorter.php create mode 100644 panel/app/src/panel/models/page/uploader.php create mode 100644 panel/app/src/panel/models/site.php create mode 100644 panel/app/src/panel/models/user.php create mode 100644 panel/app/src/panel/models/user/avatar.php create mode 100644 panel/app/src/panel/models/user/blueprint.php create mode 100644 panel/app/src/panel/models/user/history.php create mode 100644 panel/app/src/panel/roots.php create mode 100644 panel/app/src/panel/search.php create mode 100644 panel/app/src/panel/snippet.php create mode 100644 panel/app/src/panel/structure.php create mode 100644 panel/app/src/panel/structure/store.php create mode 100644 panel/app/src/panel/topbar.php create mode 100644 panel/app/src/panel/translation.php create mode 100644 panel/app/src/panel/upload.php create mode 100644 panel/app/src/panel/urls.php create mode 100644 panel/app/src/panel/view.php create mode 100644 panel/app/src/panel/widgets.php create mode 100644 panel/app/topbars/error.php create mode 100644 panel/app/topbars/user.php create mode 100644 panel/app/translations/ar/core.json create mode 100644 panel/app/translations/ar/package.json create mode 100644 panel/app/translations/bg/core.json create mode 100644 panel/app/translations/bg/package.json create mode 100644 panel/app/translations/ca/core.json create mode 100644 panel/app/translations/ca/package.json create mode 100644 panel/app/translations/cs/core.json create mode 100644 panel/app/translations/cs/package.json create mode 100644 panel/app/translations/da/core.json create mode 100644 panel/app/translations/da/package.json create mode 100644 panel/app/translations/de/core.json create mode 100644 panel/app/translations/de/package.json create mode 100644 panel/app/translations/en/core.json create mode 100644 panel/app/translations/en/package.json create mode 100644 panel/app/translations/es_419/core.json create mode 100644 panel/app/translations/es_419/package.json create mode 100644 panel/app/translations/es_ES/core.json create mode 100644 panel/app/translations/es_ES/package.json create mode 100644 panel/app/translations/fa/core.json create mode 100644 panel/app/translations/fa/package.json create mode 100644 panel/app/translations/fi/core.json create mode 100644 panel/app/translations/fi/package.json create mode 100644 panel/app/translations/fr/core.json create mode 100644 panel/app/translations/fr/package.json create mode 100644 panel/app/translations/hu/core.json create mode 100644 panel/app/translations/hu/package.json create mode 100644 panel/app/translations/id/core.json create mode 100644 panel/app/translations/id/package.json create mode 100644 panel/app/translations/it/core.json create mode 100644 panel/app/translations/it/package.json create mode 100644 panel/app/translations/ja/core.json create mode 100644 panel/app/translations/ja/package.json create mode 100644 panel/app/translations/nb/core.json create mode 100644 panel/app/translations/nb/package.json create mode 100644 panel/app/translations/nl/core.json create mode 100644 panel/app/translations/nl/package.json create mode 100644 panel/app/translations/pl/core.json create mode 100644 panel/app/translations/pl/package.json create mode 100644 panel/app/translations/pt_BR/core.json create mode 100644 panel/app/translations/pt_BR/package.json create mode 100644 panel/app/translations/pt_PT/core.json create mode 100644 panel/app/translations/pt_PT/package.json create mode 100644 panel/app/translations/ro/core.json create mode 100644 panel/app/translations/ro/package.json create mode 100644 panel/app/translations/ru/core.json create mode 100644 panel/app/translations/ru/package.json create mode 100644 panel/app/translations/sv_SE/core.json create mode 100644 panel/app/translations/sv_SE/package.json create mode 100644 panel/app/translations/tr/core.json create mode 100644 panel/app/translations/tr/package.json create mode 100644 panel/app/translations/zh_CN/core.json create mode 100644 panel/app/translations/zh_CN/package.json create mode 100644 panel/app/translations/zh_TW/core.json create mode 100644 panel/app/translations/zh_TW/package.json create mode 100644 panel/app/views/auth/block.php create mode 100644 panel/app/views/auth/error.php create mode 100644 panel/app/views/auth/login.php create mode 100644 panel/app/views/avatars/delete.php create mode 100644 panel/app/views/dashboard/index.php create mode 100644 panel/app/views/error/index.php create mode 100644 panel/app/views/error/modal.php create mode 100644 panel/app/views/files/delete.php create mode 100644 panel/app/views/files/edit.php create mode 100644 panel/app/views/files/index.php create mode 100644 panel/app/views/installation/index.php create mode 100644 panel/app/views/options/index.php create mode 100644 panel/app/views/pages/add.php create mode 100644 panel/app/views/pages/delete.php create mode 100644 panel/app/views/pages/edit.php create mode 100644 panel/app/views/pages/template.php create mode 100644 panel/app/views/pages/toggle.php create mode 100644 panel/app/views/pages/url.php create mode 100644 panel/app/views/search/results.php create mode 100644 panel/app/views/subpages/index.php create mode 100644 panel/app/views/users/delete.php create mode 100644 panel/app/views/users/edit.php create mode 100644 panel/app/views/users/index.php create mode 100644 panel/app/widgets/account/account.html.php create mode 100644 panel/app/widgets/account/account.php create mode 100644 panel/app/widgets/history/history.html.php create mode 100644 panel/app/widgets/history/history.php create mode 100644 panel/app/widgets/license/license.html.php create mode 100644 panel/app/widgets/license/license.php create mode 100644 panel/app/widgets/pages/pages.html.php create mode 100644 panel/app/widgets/pages/pages.php create mode 100644 panel/app/widgets/site/site.html.php create mode 100644 panel/app/widgets/site/site.php create mode 100644 panel/assets/css/form.min.css create mode 100644 panel/assets/css/panel.min.css create mode 100644 panel/assets/fonts/fontawesome-webfont.woff create mode 100644 panel/assets/fonts/fontawesome-webfont.woff2 create mode 100644 panel/assets/fonts/sourcesanspro-400-italic.woff create mode 100755 panel/assets/fonts/sourcesanspro-400-italic.woff2 create mode 100644 panel/assets/fonts/sourcesanspro-400.woff create mode 100755 panel/assets/fonts/sourcesanspro-400.woff2 create mode 100644 panel/assets/fonts/sourcesanspro-600.woff create mode 100755 panel/assets/fonts/sourcesanspro-600.woff2 create mode 100644 panel/assets/images/avatar.png create mode 100644 panel/assets/images/hint.arrows.png create mode 100644 panel/assets/images/loader.black.gif create mode 100644 panel/assets/images/loader.white.gif create mode 100644 panel/assets/images/pattern.png create mode 100644 panel/assets/images/placeholder.png create mode 100644 panel/assets/js/dist/app.min.js create mode 100644 panel/assets/js/dist/form.min.js create mode 100644 panel/assets/js/dist/panel.min.js create mode 100644 panel/index.php create mode 100644 panel/readme.md diff --git a/.gitmodules b/.gitmodules index d4d9a59..3ab1269 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,3 @@ -[submodule "panel"] - path = panel - url = https://github.com/getkirby/panel.git -[submodule "kirby"] - path = kirby - url = https://github.com/getkirby/kirby.git [submodule "site/fields/markdown"] path = site/fields/markdown url = https://github.com/JonasDoebertin/kirby-visual-markdown.git diff --git a/README.md b/README.md index 4d1abad..0815586 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # Sterzy.com This is the code for the http://sterzy.com website. It is based on the kirby cms (http://getkirby.com), which has it's own license agreement ([Kirby End User License Agreement](https://github.com/getkirby/starterkit/blob/master/license.md)). - The rest of the code is under this license: [License](https://svs.ankaa.uberspace.de/sterzy/sterzycom/blob/master/LICENSE "license"), [CC BY 4.0](http://creativecommons.org/licenses/by/4.0/). # Installing diff --git a/index.php b/index.php index feef4bf..77ab730 100644 --- a/index.php +++ b/index.php @@ -1,5 +1,5 @@ __DIR__ . DS . 'kirby.php', + 'kirby\\roots' => __DIR__ . DS . 'kirby' . DS . 'roots.php', + 'kirby\\urls' => __DIR__ . DS . 'kirby' . DS . 'urls.php', + 'kirby\\component' => __DIR__ . DS . 'kirby' . DS . 'component.php', + 'kirby\\registry' => __DIR__ . DS . 'kirby' . DS . 'registry.php', + 'kirby\\request' => __DIR__ . DS . 'kirby' . DS . 'request.php', + 'kirby\\request\\params' => __DIR__ . DS . 'kirby' . DS . 'request' . DS . 'params.php', + 'kirby\\request\\query' => __DIR__ . DS . 'kirby' . DS . 'request' . DS . 'query.php', + 'kirby\\request\\path' => __DIR__ . DS . 'kirby' . DS . 'request' . DS . 'path.php', + + // core components + 'kirby\\component\\template' => __DIR__ . DS . 'kirby' . DS . 'component' . DS . 'template.php', + 'kirby\\component\\thumb' => __DIR__ . DS . 'kirby' . DS . 'component' . DS . 'thumb.php', + 'kirby\\component\\markdown' => __DIR__ . DS . 'kirby' . DS . 'component' . DS . 'markdown.php', + 'kirby\\component\\smartypants' => __DIR__ . DS . 'kirby' . DS . 'component' . DS . 'smartypants.php', + 'kirby\\component\\snippet' => __DIR__ . DS . 'kirby' . DS . 'component' . DS . 'snippet.php', + 'kirby\\component\\css' => __DIR__ . DS . 'kirby' . DS . 'component' . DS . 'css.php', + 'kirby\\component\\js' => __DIR__ . DS . 'kirby' . DS . 'component' . DS . 'js.php', + 'kirby\\component\\tinyurl' => __DIR__ . DS . 'kirby' . DS . 'component' . DS . 'tinyurl.php', + 'kirby\\component\\response' => __DIR__ . DS . 'kirby' . DS . 'component' . DS . 'response.php', + + // traits + 'kirby\\traits\\image' => __DIR__ . DS . 'kirby' . DS . 'traits' . DS . 'image.php', + + // all core abstracts + 'assetabstract' => __DIR__ . DS . 'core' . DS . 'asset.php', + 'avatarabstract' => __DIR__ . DS . 'core' . DS . 'avatar.php', + 'pagesabstract' => __DIR__ . DS . 'core' . DS . 'pages.php', + 'childrenabstract' => __DIR__ . DS . 'core' . DS . 'children.php', + 'contentabstract' => __DIR__ . DS . 'core' . DS . 'content.php', + 'fieldabstract' => __DIR__ . DS . 'core' . DS . 'field.php', + 'fileabstract' => __DIR__ . DS . 'core' . DS . 'file.php', + 'filesabstract' => __DIR__ . DS . 'core' . DS . 'files.php', + 'kirbytextabstract' => __DIR__ . DS . 'core' . DS . 'kirbytext.php', + 'kirbytagabstract' => __DIR__ . DS . 'core' . DS . 'kirbytag.php', + 'pageabstract' => __DIR__ . DS . 'core' . DS . 'page.php', + 'roleabstract' => __DIR__ . DS . 'core' . DS . 'role.php', + 'rolesabstract' => __DIR__ . DS . 'core' . DS . 'roles.php', + 'siteabstract' => __DIR__ . DS . 'core' . DS . 'site.php', + 'usersabstract' => __DIR__ . DS . 'core' . DS . 'users.php', + 'userabstract' => __DIR__ . DS . 'core' . DS . 'user.php', + + // lib + 'pageextension' => __DIR__ . DS . 'lib' . DS . 'pageextension.php', + 'structure' => __DIR__ . DS . 'lib' . DS . 'structure.php', + + // parsedown + 'parsedown' => __DIR__ . DS . 'vendors' . DS . 'parsedown.php', + 'parsedownextra' => __DIR__ . DS . 'vendors' . DS . 'parsedownextra.php', + + // smartypants + 'smartypantstypographer_parser' => __DIR__ . DS . 'vendors' . DS . 'smartypants.php', + +)); + +// load all helper functions +include(__DIR__ . DS . 'helpers.php'); diff --git a/kirby/branches/default.php b/kirby/branches/default.php new file mode 100644 index 0000000..6f27318 --- /dev/null +++ b/kirby/branches/default.php @@ -0,0 +1,18 @@ + __DIR__ . DS . 'multilang' . DS . 'content.php', + 'field' => __DIR__ . DS . 'multilang' . DS . 'field.php', + 'file' => __DIR__ . DS . 'multilang' . DS . 'file.php', + 'language' => __DIR__ . DS . 'multilang' . DS . 'language.php', + 'languages' => __DIR__ . DS . 'multilang' . DS . 'languages.php', + 'page' => __DIR__ . DS . 'multilang' . DS . 'page.php', + 'site' => __DIR__ . DS . 'multilang' . DS . 'site.php', +)); \ No newline at end of file diff --git a/kirby/branches/multilang/content.php b/kirby/branches/multilang/content.php new file mode 100644 index 0000000..c80959c --- /dev/null +++ b/kirby/branches/multilang/content.php @@ -0,0 +1,41 @@ +name = f::name($this->name); + $this->language = $language; + + } + + public function realroot() { + return dirname($this->root()) . DS . $this->name() . '.' . $this->language . '.' . f::extension($this->root()); + } + + public function exists() { + return file_exists($this->realroot()); + } + + public function language() { + + if(!is_null($this->language)) return $this->language; + + $codes = $this->page->site()->languages()->codes(); + $code = f::extension(f::name($this->root)); + + return $this->language = in_array($code, $codes) ? $this->page->site()->languages()->find($code) : false; + + } + +} \ No newline at end of file diff --git a/kirby/branches/multilang/field.php b/kirby/branches/multilang/field.php new file mode 100644 index 0000000..d8dbeec --- /dev/null +++ b/kirby/branches/multilang/field.php @@ -0,0 +1,34 @@ +page->site(); + + // use current language if $lang not set + if(is_null($lang)) $lang = $site->language()->code(); + + // if language is default/fallback language + if($site->language($lang)->default()) return true; + + $current = $this->page->content($lang); + $default = $this->page->content($site->defaultLanguage->code); + + $field = $current->get($this->key); + $untranslated = $default->get($this->key)->value(); + + return $field->isNotEmpty() and $field->value() !== $untranslated; + + } + + +} diff --git a/kirby/branches/multilang/file.php b/kirby/branches/multilang/file.php new file mode 100644 index 0000000..eec16fb --- /dev/null +++ b/kirby/branches/multilang/file.php @@ -0,0 +1,170 @@ +page->textfile($this->filename(), $lang); + } + + /** + * Get the meta information + * + * @param string $lang optional language code + * @return Content + */ + public function meta($lang = null) { + + // get the content for the current language + if(is_null($lang)) { + + // the current language's content can be cached + if(isset($this->cache['meta'])) return $this->cache['meta']; + + // get the current content + $meta = $this->_meta($this->site->language->code); + + // get the fallback content + if($this->site->language->code != $this->site->defaultLanguage->code) { + + // fetch the default language content + $defaultMeta = $this->_meta($this->site->defaultLanguage->code); + + // replace all missing fields with values from the default content + foreach($defaultMeta->data as $key => $field) { + if(empty($meta->data[$key]->value)) { + $meta->data[$key] = $field; + } + } + + } + + // cache the meta for this language + return $this->cache['meta'] = $meta; + + // get the meta for another language + } else { + return $this->_meta($lang); + } + + } + + /** + * Private method to simplify meta fetching + * + * @return Content + */ + protected function _meta($lang) { + + // get the inventory + $inventory = $this->page->inventory(); + + // try to fetch the content for this language + $meta = isset($inventory['meta'][$this->filename][$lang]) ? $inventory['meta'][$this->filename][$lang] : null; + + // try to replace empty content with the default language content + if(empty($meta) and isset($inventory['meta'][$this->filename][$this->site->defaultLanguage->code])) { + $meta = $inventory['meta'][$this->filename][$this->site->defaultLanguage->code]; + } + + // find and cache the content for this language + return new Content($this->page, $this->page->root() . DS . $meta, $lang); + + } + + /** + * Renames the file and also its meta info txt + * + * @param string $filename + * @param boolean $safeName + */ + public function rename($name, $safeName = true) { + + $filename = $this->createNewFilename($name, $safeName); + $root = $this->dir() . DS . $filename; + + if(empty($name)) { + throw new Exception('The filename is missing'); + } + + if($root == $this->root()) return $filename; + + if(file_exists($root)) { + throw new Exception('A file with that name already exists'); + } + + if(!f::move($this->root(), $root)) { + throw new Exception('The file could not be renamed'); + } + + foreach($this->site->languages() as $lang) { + + // rename all meta files + $meta = $this->textfile($lang->code()); + + if(file_exists($meta)) { + f::move($meta, $this->page->textfile($filename, $lang->code())); + } + + } + + // reset the page cache + $this->page->reset(); + + // reset the basics + $this->root = $root; + $this->filename = $filename; + $this->name = $name; + $this->cache = array(); + + cache::flush(); + + return $filename; + + } + + public function update($data = array(), $lang = null) { + + $data = array_merge((array)$this->meta()->toArray(), $data); + + foreach($data as $k => $v) { + if(is_null($v)) unset($data[$k]); + } + + if(!data::write($this->textfile($lang), $data, 'kd')) { + throw new Exception('The file data could not be saved'); + } + + // reset the page cache + $this->page->reset(); + + // reset the file cache + $this->cache = array(); + + cache::flush(); + return true; + + } + + public function delete() { + + foreach($this->site->languages() as $lang) { + // delete the meta file for each language + f::remove($this->textfile($lang->code())); + } + + parent::delete(); + + return true; + + } + + +} \ No newline at end of file diff --git a/kirby/branches/multilang/language.php b/kirby/branches/multilang/language.php new file mode 100644 index 0000000..18c6368 --- /dev/null +++ b/kirby/branches/multilang/language.php @@ -0,0 +1,34 @@ +site = $site; + $this->code = $lang['code']; + $this->name = $lang['name']; + $this->locale = $lang['locale']; + $this->default = (isset($lang['default']) and $lang['default']); + $this->direction = (isset($lang['direction']) and $lang['direction'] == 'rtl') ? 'rtl' : 'ltr'; + $this->url = isset($lang['url']) ? $lang['url'] : $lang['code']; + + } + + public function url() { + return url::makeAbsolute($this->url, $this->site->url()); + } + + public function isDefault() { + return $this->default; + } + + public function __toString() { + return $this->code; + } + +} diff --git a/kirby/branches/multilang/languages.php b/kirby/branches/multilang/languages.php new file mode 100644 index 0000000..cb1cd43 --- /dev/null +++ b/kirby/branches/multilang/languages.php @@ -0,0 +1,28 @@ +site = $site; + } + + public function find($code) { + return isset($this->data[$code]) ? $this->data[$code] : null; + } + + public function codes() { + return $this->keys(); + } + + public function findDefault() { + return $this->site->defaultLanguage(); + } + +} \ No newline at end of file diff --git a/kirby/branches/multilang/page.php b/kirby/branches/multilang/page.php new file mode 100644 index 0000000..bd25bc5 --- /dev/null +++ b/kirby/branches/multilang/page.php @@ -0,0 +1,281 @@ +intendedTemplate(); + if(is_null($lang)) $lang = $this->site->language->code; + return textfile($this->diruri(), $template, $lang); + } + + /** + * Returns the translated URI + */ + public function uri($lang = null) { + // build the page's uri with the parent uri and the page's slug + return ltrim($this->parent->uri($lang) . '/' . $this->slug($lang), '/'); + } + + /** + * Returns the URL key from the content file + * if available and otherwise returns the page UID + * + * @param string $lang + * @return string + */ + public function urlKey($lang = null) { + + if($content = $this->content($lang)) { + // search for a translated url_key in that language + if($key = (string)a::get((array)$content->data(), 'url_key')) { + // if available, use the translated url key as slug + return $key; + } + } + + return $this->uid(); + + } + + /** + * Returns the slug for the page + * The slug is the last part of the URL path + * For multilang sites this can be translated with a URL-Key field + * in the text file for this page. + * + * @param string $lang Optional language code to get the translated slug + * @return string i.e. 01-projects returns projects + */ + public function slug($lang = null) { + + $default = $this->site->defaultLanguage->code; + $current = $this->site->language->code; + + // get the slug for the current language + if(is_null($lang)) { + + // the current language's slug can be cached + if(isset($this->cache['slug'])) return $this->cache['slug']; + + // if the current language is the default language + // simply return the uid + if($current == $default) { + return $this->cache['slug'] = $this->uid(); + } + + // get the translated url key + return $this->urlKey(); + + } else { + + // if the passed language code is the current language code + // we can simply return the slug method without a language code specified + if($lang == $current) { + return $this->slug(); + } + + // the slug for the default language is just the name of the folder + if($lang == $default) { + return $this->uid(); + } + + // get the translated url key + return $this->urlKey($lang); + + } + + } + + /** + * Returns the full url for the page + * + * @param string $lang Optional language code to get the URL for that specific language on multilang sites + * @return string + */ + public function url() { + + $args = func_get_args(); + $lang = array_shift($args); + + // for multi language sites every url needs + // to be treated specially to make sure each uid is translated properly + // and language codes are prepended if needed + if(is_null($lang)) { + // get the current language + $lang = $this->site->language->code; + } + + // Kirby is trying to remove the home folder name from the url + if($this->isHomePage()) { + return $this->site->url($lang); + } else if($this->parent->isHomePage()) { + return $this->site->url($lang) . '/' . $this->parent->slug($lang) . '/' . $this->slug($lang); + } else { + return $this->parent->url($lang) . '/' . $this->slug($lang); + } + + } + + /** + * Modified inventory fetcher + * + * @return array + */ + public function inventory() { + + $inventory = parent::inventory(); + $defaultLang = $this->site->defaultLanguage->code; + $expression = '!(.*?)(\.(' . implode('|', $this->site->languages->codes()) . ')|)\.' . $this->kirby->options['content.file.extension'] . '$!i'; + + foreach($inventory['meta'] as $key => $meta) { + $inventory['meta'][$key] = array($defaultLang => $meta); + } + + foreach($inventory['content'] as $key => $content) { + + preg_match($expression, $content, $match); + + $file = $match[1]; + $lang = isset($match[3]) ? $match[3] : null; + + if(in_array($file, $inventory['files'])) { + $inventory['meta'][$file][$lang] = $content; + } else { + + if(is_null($lang)) { + $lang = f::extension($file); + if(empty($lang)) $lang = $defaultLang; + } + + $inventory['content'][$lang] = $content; + } + + unset($inventory['content'][$key]); + + } + + // try to fill the default language with something else + if(!isset($inventory['content'][$defaultLang])) { + $inventory['content'][$defaultLang] = a::first($inventory['content']); + } + + return $inventory; + + } + + /** + * Returns the content object for this page + * + * @param string $lang optional language code + * @return Content + */ + public function content($lang = null) { + + // get the content for the current language + if(is_null($lang)) { + + // the current language's content can be cached + if(isset($this->cache['content'])) return $this->cache['content']; + + // get the current content + $content = $this->_content($this->site->language->code); + + // get the fallback content + if($this->site->language->code != $this->site->defaultLanguage->code) { + + // fetch the default language content + $defaultContent = $this->_content($this->site->defaultLanguage->code); + + // replace all missing fields with values from the default content + foreach($defaultContent->data as $key => $field) { + if(empty($content->data[$key]->value)) { + $content->data[$key] = $field; + } + } + + } + + // find and cache the content for this language + return $this->cache['content'] = $content; + + // get the content for another language + } else { + return $this->_content($lang); + } + + } + + /** + * Private method to simplify content fetching + * + * @return Content + */ + protected function _content($lang) { + + // get the inventory + $inventory = $this->inventory(); + + // try to fetch the content for this language + $content = isset($inventory['content'][$lang]) ? $inventory['content'][$lang] : null; + + // try to replace empty content with the default language content + if(empty($content) and isset($inventory['content'][$this->site->defaultLanguage->code])) { + $content = $inventory['content'][$this->site->defaultLanguage->code]; + } + + // find and cache the content for this language + return new Content($this, $this->root() . DS . $content, $lang); + + } + + /** + * Creates a new page object + * + * @param string $uri + * @param string $template + * @param array $data + */ + static public function create($uri, $template, $data = array()) { + return parent::create($uri, $template . '.' . site()->defaultLanguage->code, $data); + } + + /** + * Update the page with a new set of data + * + * @param array $data + */ + public function update($input = array(), $lang = null) { + + $data = a::update($this->content($lang)->toArray(), $input); + + if(!data::write($this->textfile(null, $lang), $data, 'kd')) { + throw new Exception('The page could not be updated'); + } + + $this->kirby->cache()->flush(); + $this->reset(); + $this->touch(); + return true; + + } + + /** + * Returns the name of the content text file / intended template + * So even if there's no such template it will return the intended name. + * + * @return string + */ + public function intendedTemplate() { + if(isset($this->cache['intendedTemplate'])) return $this->cache['intendedTemplate']; + return $this->cache['intendedTemplate'] = $this->content($this->site->defaultLanguage()->code())->exists() ? $this->content()->name() : 'default'; + } + +} \ No newline at end of file diff --git a/kirby/branches/multilang/site.php b/kirby/branches/multilang/site.php new file mode 100644 index 0000000..df465dd --- /dev/null +++ b/kirby/branches/multilang/site.php @@ -0,0 +1,193 @@ +languages = new Languages($this); + + foreach($kirby->options['languages'] as $lang) { + + $language = new Language($this, $lang); + + // store the default language + if($language->default) $this->defaultLanguage = $this->language = $language; + + // add the language to the collection + $this->languages->data[$language->code] = $language; + + } + + } + + /** + * Returns the translated URI + */ + public function uri($lang = null) { + return null; + } + + public function slug($lang = null) { + return null; + } + + /** + * Returns the url of the site + * + * @return string + */ + public function url($lang = false) { + if($lang) { + // return the specific language url + return $this->languages->find($lang)->url(); + } else { + return parent::url(); + } + } + + /** + * Marks the site as a multilanguage site + * + * @return boolean + */ + public function multilang() { + return true; + } + + /** + * Returns the Languages Collection + * + * @return Languages + */ + public function languages() { + return $this->languages; + } + + /** + * Returns the current language + * or any other language by language code + * + * @param string $code + * @return Language + */ + public function language($code = null) { + if(is_null($code)) return $this->language; + return $this->languages()->find($code); + } + + /** + * Returns the default language + * + * @return Language + */ + public function defaultLanguage() { + return $this->defaultLanguage; + } + + /** + * Tries to find the language for the current visitor + * + * @return Language + */ + public function visitorLanguage() { + return $this->languages()->find(visitor::acceptedLanguageCode()); + } + + /** + * Returns the detected language + * + * @return Language + */ + public function detectedLanguage() { + + if($language = $this->visitorLanguage()) { + return $language; + } else { + return $this->defaultLanguage(); + } + + } + + /** + * Returns the language which will be + * remembered for the next visit + * + * @return Language + */ + public function sessionLanguage() { + if($code = s::get('language') and $language = $this->languages()->find($code)) { + return $language; + } else { + return null; + } + } + + public function switchLanguage(Language $language) { + + s::set('language', $language->code()); + + if($this->language()->code() != $language->code()) { + go($this->page()->url($language->code())); + } + + } + + /** + * Sets the currently active page + * and returns its page object + * + * @param string $uri + * @return Page + */ + public function visit($uri = '', $lang = null) { + + // if the language code is missing or the code is invalid (TODO) + if(!in_array($lang, $this->languages()->keys())) { + $lang = $this->defaultLanguage->code; + } + + // set the current language + $this->language = $this->languages()->data[$lang]; + + // clean the uri + $uri = trim($uri, '/'); + + if(empty($uri)) { + return $this->page = $this->homePage(); + } else { + + if($lang == $this->defaultLanguage->code and $page = $this->children()->find($uri)) { + return $this->page = $page; + } else if($page = $this->children()->findByURI($uri)) { + return $this->page = $page; + } else { + return $this->page = $this->errorPage(); + } + } + + } + + /** + * Returns the locale for the site + * + * @return string + */ + public function locale() { + return $this->language->locale; + } + +} diff --git a/kirby/core/asset.php b/kirby/core/asset.php new file mode 100644 index 0000000..a92e457 --- /dev/null +++ b/kirby/core/asset.php @@ -0,0 +1,21 @@ +kirby = kirby::instance(); + if(is_a($path, 'Media')) { + parent::__construct($path->root(), $path->url()); + } else { + parent::__construct( + url::isAbsolute($path) ? null : $this->kirby->roots()->index() . DS . ltrim($path, DS), + url::makeAbsolute($path) + ); + } + } + +} \ No newline at end of file diff --git a/kirby/core/avatar.php b/kirby/core/avatar.php new file mode 100644 index 0000000..e17d7ef --- /dev/null +++ b/kirby/core/avatar.php @@ -0,0 +1,30 @@ +user = $user; + + // this should rather be coming from the user object + $this->kirby = kirby::instance(); + + // try to find the avatar + if($file = f::resolve($this->kirby->roots()->avatars() . DS . $user->username(), ['jpg', 'jpeg', 'gif', 'png'])) { + $filename = f::filename($file); + } else { + $filename = $user->username() . '.jpg'; + $file = $this->kirby->roots()->avatars() . DS . $filename; + } + + parent::__construct($file, $this->kirby->urls()->avatars() . '/' . $filename); + + } + +} \ No newline at end of file diff --git a/kirby/core/children.php b/kirby/core/children.php new file mode 100644 index 0000000..67ae247 --- /dev/null +++ b/kirby/core/children.php @@ -0,0 +1,207 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class ChildrenAbstract extends Pages { + + protected $page = null; + protected $cache = array(); + + /** + * Constructor + */ + public function __construct($page) { + $this->page = $page; + } + + /** + * Creates a new Page object and adds it to the collection + */ + public function add($dirname) { + $page = new Page($this->page, $dirname); + $this->data[$page->id()] = $page; + return $page; + } + + /** + * Creates a new subpage + * + * @param string $uid + * @param string $template + * @param array $data + */ + public function create($uid, $template, $data = array()) { + $page = page::create($this->page->id() . '/' . $uid, $template, $data); + $this->data[$page->id()] = $page; + return $page; + } + + /** + * Returns the parent page + * + * @return Page + */ + public function page() { + return $this->page; + } + + /** + * Returns the Children of Children + * + * @return Children + */ + public function children() { + $grandChildren = new Children($this->page); + foreach($this->data as $page) { + foreach($page->children() as $subpage) { + $grandChildren->data[$subpage->id()] = $subpage; + } + } + return $grandChildren; + } + + /** + * Find a specific page by its uri + * + * @return Page or false + */ + public function find() { + + $args = func_get_args(); + + if(!count($args)) { + return false; + } else if (count($args) === 1 and is_array($args[0])) { + $args = $args[0]; + } + + if(count($args) > 1) { + $collection = new Children($this->page); + foreach($args as $id) { + if($page = $this->find($id)) { + $collection->data[$page->id()] = $page; + } + } + return $collection; + } else { + + // get the first argument and remove slashes + $id = trim($args[0], '/'); + + // build the direct uri + $directId = trim($this->page()->id() . '/' . $id, '/'); + + // fast access to direct uris + if(isset($this->data[$directId])) return $this->data[$directId]; + + $path = explode('/', $id); + $obj = $this; + $page = false; + + foreach($path as $uid) { + + $id = ltrim($obj->page()->id() . '/' . $uid, '/'); + + if(!isset($obj->data[$id])) return false; + + $page = $obj->data[$id]; + $obj = $page->children(); + + } + + return $page; + + } + + } + + /** + * Finds pages by it's unique URI + * + * @param mixed $uri Either a single URI string or an array of URIs + * @param string $use The field, which should be used (uid or slug) + * @return mixed Either a Page object, a Pages object for multiple pages or null if nothing could be found + */ + public function findByURI() { + + $args = func_get_args(); + + if(count($args) == 0) { + return false; + } else if(count($args) > 1) { + $collection = new Children($this->page); + foreach($args as $uri) { + $page = $this->findByURI($uri); + if($page) $collection->data[$page->id()] = $page; + } + return $collection; + } else { + + // get the first argument and remove slashes + $uri = trim($args[0], '/'); + $array = str::split($uri, '/'); + $obj = $this; + $page = false; + + foreach($array as $p) { + + $next = $obj->findBy('slug', $p); + + if(!$next) break; + + $page = $next; + $obj = $next->children(); + + } + + return ($page and $page->slug() != a::last($array)) ? false : $page; + + } + + } + + /** + * Creates a clean one-level collection with all + * pages, subpages, subsubpages, etc. + * + * @param object Pages object for recursive indexing + * @return Children + */ + public function index(Children $obj = null) { + + if(is_null($obj)) { + if(isset($this->cache['index'])) return $this->cache['index']; + $this->cache['index'] = new Children($this->page); + $obj = $this; + } + + foreach($obj->data as $key => $page) { + $this->cache['index']->data[$page->uri()] = $page; + $this->index($page->children()); + } + + return $this->cache['index']; + + } + + /** + * Extended group method + * detaches children and converts them to + * a simple pages collection + * + * @param function $callback + * @return Pages + */ + public function group($callback) { + $collection = new Pages($this); + return $collection->group($callback); + } + +} \ No newline at end of file diff --git a/kirby/core/content.php b/kirby/core/content.php new file mode 100644 index 0000000..0a6bac6 --- /dev/null +++ b/kirby/core/content.php @@ -0,0 +1,151 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class ContentAbstract { + + public $page = null; + public $root = null; + public $raw = null; + public $data = array(); + public $fields = array(); + public $name = null; + + /** + * Constructor + */ + public function __construct($page, $root) { + + $this->page = $page; + $this->root = $root; + $this->name = pathinfo($root, PATHINFO_FILENAME); + + // stop at invalid files + if(empty($this->root) or !is_file($this->root) or !is_readable($this->root)) return; + + // read the content file and remove the BOM + $this->raw = str_replace(BOM, '', file_get_contents($this->root)); + + // explode all fields by the line separator + $fields = preg_split('!\n----\s*\n*!', $this->raw); + + // loop through all fields and add them to the content + foreach($fields as $field) { + $pos = strpos($field, ':'); + $key = str_replace(array('-', ' '), '_', strtolower(trim(substr($field, 0, $pos)))); + + // Don't add fields with empty keys + if(empty($key)) continue; + + // add the key to the fields list + $this->fields[] = $key; + + // add the key object + $this->data[$key] = new Field($this->page, $key, trim(substr($field, $pos+1))); + } + + } + + /** + * Returns the root for the content file + */ + public function root() { + return $this->root; + } + + /** + * Returns the name of the content file + * without the extension. This is + * being used to determine the template for the page + * + * @return string + */ + public function name() { + return $this->name; + } + + /** + * Returns an array with all + * field names + * + * @return array3 + */ + public function fields() { + return $this->fields; + } + + /** + * Returns the raw content from the file + * + * @return string + */ + public function raw() { + return $this->raw; + } + + /** + * Returns the entire data array + * with all field objects + * + * @return array + */ + public function data() { + return $this->data; + } + + /** + * Checks if the content file exists + * + * @return boolean + */ + public function exists() { + return file_exists($this->root); + } + + /** + * Gets a field from the content + * + * @return Field + */ + public function get($key, $arguments = null) { + + // case-insensitive data fetching + $key = strtolower($key); + + if(isset($this->data[$key])) { + return $this->data[$key]; + } else { + // return an empty field as default + return new Field($this->page, $key); + } + + } + + /** + * Checks if a field exists + * + * @param string $key + * @return boolean + */ + public function has($key) { + return isset($this->data[strtolower($key)]); + } + + public function __call($method, $arguments = null) { + return $this->get($method, $arguments); + } + + public function toArray() { + return array_map(function($item) { + return $item->value; + }, $this->data); + } + +} \ No newline at end of file diff --git a/kirby/core/field.php b/kirby/core/field.php new file mode 100644 index 0000000..ba8127c --- /dev/null +++ b/kirby/core/field.php @@ -0,0 +1,55 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class FieldAbstract { + + static public $methods = array(); + + public $page; + public $key; + public $value; + + public function __construct($page, $key, $value = '') { + $this->page = $page; + $this->key = $key; + $this->value = $value; + } + + public function page() { + return $this->page; + } + public function exists() { + return $this->page->content()->has($this->key); + } + public function key() { + return $this->key; + } + public function value() { + return $this->value; + } + public function isTranslated($lang = null) { + return true; + } + public function __toString() { + return (string)$this->value; + } + public function toString() { + return $this->value; + } + public function __call($method, $arguments = array()) { + if(isset(static::$methods[$method])) { + array_unshift($arguments, clone $this); + return call(static::$methods[$method], $arguments); + } else { + return $this; + } + } +} diff --git a/kirby/core/file.php b/kirby/core/file.php new file mode 100644 index 0000000..010f386 --- /dev/null +++ b/kirby/core/file.php @@ -0,0 +1,333 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class FileAbstract extends Media { + + use Kirby\Traits\Image; + + static public $methods = array(); + + public $kirby; + public $site; + public $page; + public $files; + + /** + * Constructor + * + * @param Files The parent files collection + * @param string The filename + */ + public function __construct(Files $files, $filename) { + + $this->kirby = $files->kirby; + $this->site = $files->site; + $this->page = $files->page; + $this->files = $files; + $this->root = $this->files->page()->root() . DS . $filename; + + parent::__construct($this->root); + + } + + /** + * Returns the kirby object + * + * @return Kirby + */ + public function kirby() { + return $this->kirby; + } + + /** + * Returns the parent site object + * + * @return Site + */ + public function site() { + return $this->site; + } + + /** + * Returns the parent page object + * + * @return Page + */ + public function page() { + return $this->page; + } + + /** + * Returns the parent files collection + * + * @return Files + */ + public function files() { + return $this->files; + } + + /** + * Returns the full root for the content file + * + * @return string + */ + public function textfile() { + return $this->page->textfile($this->filename()); + } + + public function siblings() { + return $this->files->not($this->filename); + } + + public function next() { + $siblings = $this->files; + $index = $siblings->indexOf($this); + if($index === false) return false; + return $this->files->nth($index+1); + } + + public function hasNext() { + return $this->next(); + } + + public function prev() { + $siblings = $this->files; + $index = $siblings->indexOf($this); + if($index === false) return false; + return $this->files->nth($index-1); + } + + public function hasPrev() { + return $this->prev(); + } + + /** + * Returns the absolute URL for the file + * + * @return string + */ + public function url($raw = false) { + if($raw || empty($this->modifications)) { + return $this->page->contentUrl() . '/' . rawurlencode($this->filename); + } else { + return $this->kirby->component('thumb')->url($this); + } + } + + /** + * Returns the relative URI for the image + * + * @return string + */ + public function uri() { + return $this->page->uri() . '/' . rawurlencode($this->filename); + } + + /** + * Returns the full directory path starting from the content folder + * + * @return string + */ + public function diruri() { + return $this->page->diruri() . '/' . rawurlencode($this->filename); + } + + /** + * Get the meta information + * + * @return Content + */ + public function meta() { + + if(isset($this->cache['meta'])) { + return $this->cache['meta']; + } else { + + $inventory = $this->page->inventory(); + $file = isset($inventory['meta'][$this->filename]) ? $this->page->root() . DS . $inventory['meta'][$this->filename] : null; + + return $this->cache['meta'] = new Content($this->page, $file); + + } + + } + + /** + * Custom modified method for files + * + * @param string $format + * @return string + */ + public function modified($format = null, $handler = null) { + return parent::modified($format, $handler ? $handler : $this->kirby->options['date.handler']); + } + + /** + * Magic getter for all meta fields + * + * @return Field + */ + public function __call($key, $arguments = null) { + if(isset(static::$methods[$key])) { + if(!$arguments) $arguments = array(); + array_unshift($arguments, clone $this); + return call(static::$methods[$key], $arguments); + } else { + return $this->meta()->get($key, $arguments); + } + } + + /** + * Generates a new filename for a given name + * and makes sure to handle badly given extensions correctly + * + * @param string $name + * @return string + */ + public function createNewFilename($name, $safeName = true) { + + $name = basename($safeName ? f::safeName($name) : $name); + $ext = f::extension($name); + + // remove possible extensions + if(in_array($ext, f::extensions())) { + $name = f::name($name); + } + + return trim($name . '.' . $this->extension(), '.'); + + } + + /** + * Renames the file and also its meta info txt + * + * @param string $filename + * @param boolean $safeName + */ + public function rename($name, $safeName = true) { + + $filename = $this->createNewFilename($name, $safeName); + $root = $this->dir() . DS . $filename; + + if(empty($name)) { + throw new Exception('The filename is missing'); + } + + if($root == $this->root()) return $filename; + + if(file_exists($root)) { + throw new Exception('A file with that name already exists'); + } + + if(!f::move($this->root(), $root)) { + throw new Exception('The file could not be renamed'); + } + + $meta = $this->textfile(); + + if(file_exists($meta)) { + f::move($meta, $this->page->textfile($filename)); + } + + // reset the page cache + $this->page->reset(); + + // reset the basics + $this->root = $root; + $this->filename = $filename; + $this->name = $name; + $this->cache = array(); + + cache::flush(); + + return $filename; + + } + + public function update($data = array()) { + + $data = array_merge((array)$this->meta()->toArray(), $data); + + foreach($data as $k => $v) { + if(is_null($v)) unset($data[$k]); + } + + if(!data::write($this->textfile(), $data, 'kd')) { + throw new Exception('The file data could not be saved'); + } + + // reset the page cache + $this->page->reset(); + + // reset the file cache + $this->cache = array(); + + cache::flush(); + return true; + + } + + public function delete() { + + // delete the meta file + f::remove($this->textfile()); + + if(!f::remove($this->root())) { + throw new Exception('The file could not be deleted'); + } + + cache::flush(); + return true; + + } + + /** + * Get formatted date fields + * + * @param string $format + * @param string $field + * @return mixed + */ + public function date($format = null, $field = 'date') { + if($timestamp = strtotime($this->meta()->$field())) { + if(is_null($format)) { + return $timestamp; + } else { + return $this->kirby->options['date.handler']($format, $timestamp); + } + } else { + return false; + } + } + + /** + * Converts the entire file object into + * a plain PHP array + * + * @param closure $callback Filter callback + * @return array + */ + public function toArray($callback = null) { + + $data = parent::toArray(); + + // add the meta content + $data['meta'] = $this->meta()->toArray(); + + if(is_null($callback)) { + return $data; + } else { + return array_map($callback, $data); + } + + } + +} \ No newline at end of file diff --git a/kirby/core/files.php b/kirby/core/files.php new file mode 100644 index 0000000..ec92d8e --- /dev/null +++ b/kirby/core/files.php @@ -0,0 +1,137 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class FilesAbstract extends Collection { + + static public $methods = array(); + + public $kirby = null; + public $site = null; + public $page = null; + + public function __construct($page) { + $this->kirby = $page->kirby; + $this->site = $page->site; + $this->page = $page; + $inventory = $page->inventory(); + + foreach($inventory['files'] as $filename) { + $file = new File($this, $filename); + $this->data[strtolower($file->filename())] = $file; + } + } + + public function __call($method, $arguments) { + + if(isset(static::$methods[$method])) { + array_unshift($arguments, clone $this); + return call(static::$methods[$method], $arguments); + } else { + return $this->get($method); + } + + } + + public function kirby() { + return $this->kirby; + } + + public function site() { + return $this->site; + } + + public function page() { + return $this->page; + } + + public function find() { + + $args = func_get_args(); + + if(!count($args)) { + return false; + } + + if(count($args) === 1 and is_array($args[0])) { + $args = $args[0]; + } + + if(count($args) > 1) { + $files = clone $this; + $files->data = array(); + foreach($args as $filename) { + $file = $this->find($filename); + if(!empty($file)) { + $files->data[$filename] = $file; + } + } + return $files; + } else { + $filename = strtolower($args[0]); + return isset($this->data[$filename]) ? $this->data[$filename] : null; + } + + } + + /** + * Returns a new collection of files without the given files + * + * @param args any number of filenames or file objects, passed as individual arguments + * @return object a new collection without the files + */ + public function not() { + $collection = clone $this; + foreach(func_get_args() as $filename) { + if(is_array($filename) or $filename instanceof Traversable) { + foreach($filename as $f) { + $collection = $collection->not($f); + } + } else if(is_a($filename, 'Media')) { + // unset by Media object + unset($collection->data[strtolower($filename->filename())]); + } else { + unset($collection->data[strtolower($filename)]); + } + } + return $collection; + } + + /** + * Converts the files collection + * into a plain array + * + * @param closure $callback Filter callback for each item + * @return array + */ + public function toArray($callback = null) { + + $data = array(); + + foreach($this as $file) { + $data[] = $file->toArray($callback); + } + + return $data; + + } + + /** + * Converts the files collection + * into a json string + * + * @param closure $callback Filter callback for each item + * @return string + */ + public function toJson($callback = null) { + return json_encode($this->toArray($callback)); + } + +} \ No newline at end of file diff --git a/kirby/core/kirbytag.php b/kirby/core/kirbytag.php new file mode 100644 index 0000000..8ad00fa --- /dev/null +++ b/kirby/core/kirbytag.php @@ -0,0 +1,173 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class KirbytagAbstract { + + protected $page; + protected $kirbytext; + protected $name; + protected $html; + protected $attr = array(); + + public function __construct($kirbytext, $name, $tag) { + + if(is_null($kirbytext)) $kirbytext = new Kirbytext(''); + + $this->page = $kirbytext->field->page; + $this->kirbytext = $kirbytext; + $this->name = $name; + $this->html = kirbytext::$tags[$name]['html']; + + // get a list with all attributes + $attributes = isset(kirbytext::$tags[$name]['attr']) ? (array)kirbytext::$tags[$name]['attr'] : array(); + + // add the name as first attribute + array_unshift($attributes, $name); + + if(is_array($tag)) { + foreach($attributes as $key) { + if(isset($tag[$key])) $this->attr[$key] = $tag[$key]; + } + } else { + + // extract all attributes + $search = preg_split('!(' . implode('|', $attributes) . '):!i', $tag, false, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY); + $num = 0; + + foreach($search AS $key) { + + if(!isset($search[$num+1])) break; + + $key = trim($search[$num]); + $value = trim($search[$num+1]); + + $this->attr[$key] = $value; + $num = $num+2; + + } + + } + + } + + /** + * Returns the parent active page + * + * @return object Page + */ + public function page() { + return $this->page; + } + + /** + * Returns the parent kirbytext object + * + * @return object Kirbytext + */ + public function kirbytext() { + return $this->kirbytext; + } + + /** + * Returns the field object + * + * @return object Field + */ + public function field() { + return $this->kirbytext->field(); + } + + /** + * Tries to find all related files for the current page + * + * @return object Files + */ + public function files() { + return $this->page->files(); + } + + /** + * Tries to find a file for the given url/uri + * + * @param string $url a full path to a file or just a filename for files form the current active page + * @return object File + */ + public function file($url) { + + // if this is an absolute url cancel + if(preg_match('!(http|https)\:\/\/!i', $url)) return false; + + // skip urls without extensions + if(!preg_match('!\.[a-z0-9]+$!i',$url)) return false; + + // relative url + if(str::contains($url, '/')) { + + $path = dirname($url); + $filename = basename($url); + + if($page = page($path) and $file = $page->file($filename)) { + return $file; + } else { + return false; + } + + } + + // try to get all files for the current page + $files = $this->files(); + + // cancel if no files are available + if(!$files) return false; + + // try to find the file + return $files->find($url); + + } + + /** + * Returns a specific attribute by key or all attributes + * by passing no key at all. + * + * @param mixed $key + * @param mixed $default + * @return array + */ + public function attr($key = null, $default = null) { + if(is_null($key)) return $this->attr; + return isset($this->attr[$key]) ? $this->attr[$key] : $default; + } + + /** + * Smart getter for the applicable target attribute. + * This will watch for popup or target attributes and return + * a proper target value if available. + * + * @return string + */ + public function target() { + if(empty($this->attr['popup']) and empty($this->attr['target'])) return false; + return empty($this->attr['popup']) ? $this->attr['target'] : '_blank'; + } + + public function html() { + if(!is_callable($this->html)) { + return (string)$this->html; + } else { + return call_user_func_array($this->html, array($this)); + } + } + + public function __toString() { + return (string)$this->html(); + } + +} \ No newline at end of file diff --git a/kirby/core/kirbytext.php b/kirby/core/kirbytext.php new file mode 100644 index 0000000..b46d9b9 --- /dev/null +++ b/kirby/core/kirbytext.php @@ -0,0 +1,107 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class KirbytextAbstract { + + static public $tags = array(); + static public $pre = array(); + static public $post = array(); + + public $field; + + public function __construct($field) { + + if(is_a($field, 'Field')) { + $this->field = $field; + } else if(is_array($field)) { + throw new Exception('Kirbytext cannot handle arrays'); + } else if(empty($field) or is_string($field)) { + $this->field = new Field(page(), null, $field); + } + + } + + public function field() { + return $this->field; + } + + public function parse() { + + if(!$this->field) return ''; + + $text = $this->field->value; + + // pre filters + foreach(static::$pre as $filter) { + $text = call_user_func_array($filter, array($this, $text)); + } + + // tagsify + $text = preg_replace_callback('!(?=[^\]])\([a-z0-9_-]+:.*?\)!is', array($this, 'tag'), $text); + + // markdownify + $text = kirby::instance()->component('markdown')->parse($text); + + // smartypantsify + $text = kirby::instance()->component('smartypants')->parse($text); + + // post filters + foreach(static::$post as $filter) { + $text = call_user_func_array($filter, array($this, $text)); + } + + return $text; + + } + + public function tag($input) { + + // remove the brackets + $tag = trim(rtrim(ltrim($input[0], '('), ')')); + $name = trim(substr($tag, 0, strpos($tag, ':'))); + + // if the tag is not installed return the entire string + if(!isset(static::$tags[$name])) return $input[0]; + + try { + $tag = new Kirbytag($this, $name, $tag); + return $tag->html(); + } catch(Exception $e) { + // broken tags will be ignored + return $input[0]; + } + + } + + static public function install($root) { + + if(!is_dir($root)) return false; + + foreach(scandir($root) as $file) { + if(pathinfo($file, PATHINFO_EXTENSION) == 'php') { + $name = pathinfo($file, PATHINFO_FILENAME); + $tag = include($root . DS . $file); + if(is_array($tag)) Kirbytext::$tags[$name] = $tag; + } + } + + } + + public function __toString() { + try { + return $this->parse(); + } catch(Exception $e) { + // on massive render bugs the entire text will be returned + return $this->field->value; + } + } + +} \ No newline at end of file diff --git a/kirby/core/page.php b/kirby/core/page.php new file mode 100644 index 0000000..fff6cca --- /dev/null +++ b/kirby/core/page.php @@ -0,0 +1,1443 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class PageAbstract { + + static public $models = array(); + static public $methods = array(); + + public $kirby; + public $site; + public $parent; + + protected $id; + protected $dirname; + protected $root; + protected $depth; + protected $uid; + protected $num; + protected $uri; + protected $cache = array(); + + /** + * Constructor + * + * @param Page $parent + * @param string $dirname + */ + public function __construct($parent, $dirname) { + + $this->kirby = $parent->kirby; + $this->site = $parent->site; + $this->parent = $parent; + $this->dirname = $dirname; + $this->root = $parent->root() . DS . $dirname; + $this->depth = $parent->depth() + 1; + + // extract the uid and num of the directory + if(preg_match('/^([0-9]+)[\-](.*)$/', $this->dirname, $match)) { + $this->uid = $match[2]; + $this->num = $match[1]; + } else { + $this->num = null; + $this->uid = $this->dirname; + } + + // assign the uid + $this->id = $this->uri = ltrim($parent->id() . '/' . $this->uid, '/'); + + } + + /** + * Cleans the temporary page cache and + * the cache of all parent pages + */ + public function reset() { + $this->cache = array(); + $this->parent()->reset(); + } + + /** + * Mark the page as modified + */ + public function touch() { + return touch($this->root()); + } + + /** + * Returns the kirby object + * + * @return Kirby + */ + public function kirby() { + return $this->kirby; + } + + /** + * Returns the site object + * + * @return Site + */ + public function site() { + return $this->site; + } + + /** + * Returns the parent page element + * + * @return Page + */ + public function parent() { + return $this->parent; + } + + /** + * Returns all parents + * + * @return Children + */ + public function parents() { + + if(isset($this->cache['parents'])) return $this->cache['parents']; + + $children = new Children($this->site); + $parents = array(); + $next = $this->parent(); + + while($next and $next->depth() > 0) { + $children->data[$next->id()] = $next; + $next = $next->parent(); + } + + return $this->cache['parents'] = $children; + + } + + /** + * Returns the full root of the page folder + * + * @return string + */ + public function root() { + return $this->root; + } + + /** + * Returns the name of the directory + * + * @return string + */ + public function dirname() { + return $this->dirname; + } + + /** + * Returns the relative URL for the directory. + * Relative to the base content directory + * + * @return string + */ + public function diruri() { + if(isset($this->cache['diruri'])) return $this->cache['diruri']; + return $this->cache['diruri'] = ltrim($this->parent()->diruri() . '/' . $this->dirname(), '/'); + } + + /** + * Returns the full url for the page + * + * @return string + */ + public function url() { + + if(isset($this->cache['url'])) return $this->cache['url']; + + // Kirby is trying to remove the home folder name from the url + if($this->isHomePage()) { + // return the base url + return $this->cache['url'] = $this->site->url(); + } else if($this->parent->isHomePage()) { + return $this->cache['url'] = $this->site->url() . '/' . $this->parent->uid . '/' . $this->uid; + } else { + $purl = $this->parent->url(); + return $this->cache['url'] = $purl == '/' ? '/' . $this->uid : $this->parent->url() . '/' . $this->uid; + } + + } + + /** + * Returns the full URL for the content folder + * + * @return string + */ + public function contentUrl() { + return $this->kirby()->urls()->content() . '/' . $this->diruri(); + } + + /** + * Builds and returns the short url for the current page + * + * @return string + */ + public function tinyurl() { + if(!$this->kirby->options['tinyurl.enabled']) { + return $this->url(); + } else { + return url($this->kirby->options['tinyurl.folder'] . '/' . $this->hash()); + } + } + + /** + * Returns a number indicating how deep the page + * is nested within the content folder + * + * @return int + */ + public function depth() { + return $this->depth; + } + + /** + * Returns the uri for the page + * which is being used for the url later + * + * @return string + */ + public function uri() { + return $this->uri; + } + + /** + * Returns the id, which is going to be used for + * Collection keys and things like that + * + * @return string + */ + public function id() { + return $this->id; + } + + /** + * Checks if the page can be cached + * + * @return boolean + */ + public function isCachable() { + + // The error page should not be cached + if($this->isErrorPage()) { + return false; + } + + foreach($this->kirby->option('cache.ignore') as $pattern) { + if(fnmatch($pattern, $this->uri()) === true) { + return false; + } + } + + return true; + + } + + /** + * Returns the page uid, which is the + * folder name without the sorting number + * + * @return string + */ + public function uid() { + return $this->uid; + } + + /** + * Alternative for $this->uid() + * + * @return string + */ + public function slug() { + return $this->uid; + } + + /** + * Returns the sorting number if it exists + * + * @return string + */ + public function num() { + return $this->num; + } + + /** + * Reads the directory and returns an inventory array + * + * @return array + */ + public function inventory() { + + if(isset($this->cache['inventory'])) return $this->cache['inventory']; + + // get all items within the directory + $ignore = array('.', '..', '.DS_Store', '.git', '.svn', 'Thumb.db'); + $items = array_diff(scandir($this->root), array_merge($ignore, (array)$this->kirby->option('content.file.ignore'))); + + // create the inventory + $this->cache['inventory'] = array( + 'children' => array(), + 'content' => array(), + 'meta' => array(), + 'thumbs' => array(), + 'files' => array(), + ); + + // normalize the filename if possible + if($this->kirby->option('content.file.normalize') && class_exists('Normalizer')) { + $items = array_map('Normalizer::normalize', $items); + } + + foreach($items as $item) { + + // skip any invisible files and folders + if(substr($item, 0, 1) === '.') continue; + + $root = $this->root . DS . $item; + + if(is_dir($root)) { + $this->cache['inventory']['children'][] = $item; + } else if(pathinfo($item, PATHINFO_EXTENSION) == $this->kirby->options['content.file.extension']) { + $this->cache['inventory']['content'][] = $item; + } else if(strpos($item, '.thumb.') !== false and preg_match('!\.thumb\.(jpg|jpeg|png|gif)$!i', $item)) { + // get the filename of the original image and use it as the array key + $image = str_replace('.thumb', '', $item); + // this makes it easier to find the corresponding image later + $this->cache['inventory']['thumbs'][$image] = $item; + } else { + $this->cache['inventory']['files'][] = $item; + } + + } + + foreach($this->cache['inventory']['thumbs'] as $key => $thumb) { + // remove invalid thumbs by looking for a matching image file and + if(!in_array($key, $this->cache['inventory']['files'])) { + $this->cache['inventory']['files'][] = $thumb; + unset($this->cache['inventory']['thumbs'][$key]); + } + } + + foreach($this->cache['inventory']['content'] as $key => $content) { + $file = pathinfo($content, PATHINFO_FILENAME); + if(in_array($file, $this->cache['inventory']['files'])) { + $this->cache['inventory']['meta'][$file] = $content; + unset($this->cache['inventory']['content'][$key]); + } + } + + // sort the children + natsort($this->cache['inventory']['children']); + + return $this->cache['inventory']; + + } + + /** + * Returns all children for this page + * + * @return Children + */ + public function children() { + + if(isset($this->cache['children'])) return $this->cache['children']; + + $this->cache['children'] = new Children($this); + + $inventory = $this->inventory(); + + // with page models + if(!empty(static::$models)) { + foreach($inventory['children'] as $dirname) { + $child = new Page($this, $dirname); + // let's create a model if one is defined + if(isset(static::$models[$child->intendedTemplate()])) { + $model = static::$models[$child->intendedTemplate()]; + $child = new $model($this, $dirname); + } + $this->cache['children']->data[$child->id()] = $child; + } + // without page models + } else { + foreach($inventory['children'] as $dirname) { + $child = new Page($this, $dirname); + $this->cache['children']->data[$child->id()] = $child; + } + } + + return $this->cache['children']; + + } + + /** + * Checks if the page has children + * + * @return boolean + */ + public function hasChildren() { + return $this->children()->count(); + } + + /** + * Checks if the page has visible children + * + * @return boolean + */ + public function hasVisibleChildren() { + return $this->children()->visible()->count(); + } + + /** + * Checks if the page has invisible children + * + * @return boolean + */ + public function hasInvisibleChildren() { + return $this->children()->invisible()->count(); + } + + /** + * Returns the grand children of this page + * + * @return Children + */ + public function grandChildren() { + return $this->children()->children(); + } + + /** + * Returns the siblings for this page, not including this page + * + * @param boolean $self + * @return Children + */ + public function siblings($self = true) { + return $self ? $this->parent->children() : $this->parent->children()->not($this); + } + + /** + * Internal method to return the next page + * + * @param object $siblings Children A collection of siblings to search in + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return mixed Page or null + */ + protected function _next(Children $siblings, $sort = array(), $visibility = false) { + + if($sort) $siblings = call(array($siblings, 'sortBy'), $sort); + $index = $siblings->indexOf($this); + if($index === false) return null; + if($visibility) { + $siblings = $siblings->offset($index+1); + $siblings = $siblings->{$visibility}(); + return $siblings->first(); + } else { + return $siblings->nth($index + 1); + } + } + + /** + * Internal method to return the previous page + * + * @param object $siblings Children A collection of siblings to search in + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return mixed Page or null + */ + protected function _prev(Children $siblings, $sort = array(), $visibility = false) { + if($sort) $siblings = call(array($siblings, 'sortBy'), $sort); + $index = $siblings->indexOf($this); + if($index === false or $index === 0) return null; + if($visibility) { + $siblings = $siblings->limit($index); + $siblings = $siblings->{$visibility}(); + return $siblings->last(); + } else { + return $siblings->nth($index - 1); + } + } + + /** + * Returns the next page element + * + * @return Page + */ + public function next() { + return $this->_next($this->parent->children(), func_get_args()); + } + + /** + * Checks if there's a next page + * + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return boolean + */ + public function hasNext() { + return call(array($this, 'next'), func_get_args()) != null; + } + + /** + * Returns the next visible page in the current collection if available + * + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return mixed Page or null + */ + public function nextVisible() { + if(!$this->parent) { + return null; + } else { + return $this->_next($this->parent->children(), func_get_args(), 'visible'); + } + } + + /** + * Checks if there's a next visible page + * + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return boolean + */ + public function hasNextVisible() { + return call(array($this, 'nextVisible'), func_get_args()) != null; + } + + /** + * Returns the next invisible page in the current collection if available + * + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return mixed Page or null + */ + public function nextInvisible() { + if(!$this->parent) { + return null; + } else { + return $this->_next($this->parent->children(), func_get_args(), 'invisible'); + } + } + + /** + * Checks if there's a next invisible page + * + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return boolean + */ + public function hasNextInvisible() { + return call(array($this, 'nextInvisible'), func_get_args()) != null; + } + + /** + * Returns the previous page element + * + * @return Page + */ + public function prev() { + return $this->_prev($this->parent->children(), func_get_args()); + } + + /** + * Checks if there's a previous page + * + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return boolean + */ + public function hasPrev() { + return call(array($this, 'prev'), func_get_args()) != null; + } + + /** + * Returns the previous visible page in the current collection if available + * + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return mixed Page or null + */ + public function prevVisible() { + if(!$this->parent) { + return null; + } else { + return $this->_prev($this->parent->children(), func_get_args(), 'visible'); + } + } + + /** + * Checks if there's a previous visible page + * + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return boolean + */ + public function hasPrevVisible() { + return call(array($this, 'prevVisible'), func_get_args()) != null; + } + + /** + * Returns the previous invisible page in the current collection if available + * + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return mixed Page or null + */ + public function prevInvisible() { + if(!$this->parent) { + return null; + } else { + return $this->_prev($this->parent->children(), func_get_args(), 'invisible'); + } + } + + /** + * Checks if there's a previous invisible page + * + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return boolean + */ + public function hasPrevInvisible() { + return call(array($this, 'prevInvisible'), func_get_args()) != null; + } + + /** + * Find any child or a set of children of this page + * + * @return Page | Children + */ + public function find() { + return call_user_func_array(array($this->children(), 'find'), func_get_args()); + } + + /** + * Find any file or a set of files for this page + * + * @return File | Files + */ + public function file() { + $args = func_get_args(); + if(empty($args)) return $this->files()->first(); + return call_user_func_array(array($this->files(), 'find'), $args); + } + + // file stuff + + /** + * Returns all files for this page + * + * @return Files + */ + public function files() { + if(isset($this->cache['files'])) return $this->cache['files']; + return $this->cache['files'] = new Files($this); + } + + /** + * Checks if this page has attached files + * + * @return boolean + */ + public function hasFiles() { + return $this->files()->count(); + } + + // File filters + public function images() { return $this->files()->filterBy('type', 'image'); } + public function videos() { return $this->files()->filterBy('type', 'video'); } + public function documents() { return $this->files()->filterBy('type', 'document'); } + public function audio() { return $this->files()->filterBy('type', 'audio'); } + public function code() { return $this->files()->filterBy('type', 'code'); } + public function archives() { return $this->files()->filterBy('type', 'archive'); } + + // File checkers + public function hasImages() { return $this->images()->count(); } + public function hasVideos() { return $this->videos()->count(); } + public function hasDocuments() { return $this->documents()->count(); } + public function hasAudio() { return $this->audio()->count(); } + public function hasCode() { return $this->code()->count(); } + public function hasArchives() { return $this->archives()->count(); } + + /** + * Returns a single image + * + * @return File + */ + public function image($filename = null) { + if(is_null($filename)) return $this->images()->first(); + return $this->images()->find($filename); + } + + /** + * Returns a single video + * + * @return File + */ + public function video($filename = null) { + if(is_null($filename)) return $this->videos()->first(); + return $this->videos()->find($filename); + } + + /** + * Returns a single document + * + * @return File + */ + public function document($filename = null) { + if(is_null($filename)) return $this->documents()->first(); + return $this->documents()->find($filename); + } + + + /** + * Returns the content object for this page + * + * @return Content + */ + public function content() { + + if(isset($this->cache['content'])) { + return $this->cache['content']; + } else { + $inventory = $this->inventory(); + return $this->cache['content'] = new Content($this, $this->root() . DS . array_shift($inventory['content'])); + } + + } + + /** + * Returns the title for this page and + * falls back to the uid if no title exists + * + * @return Field + */ + public function title() { + $title = $this->content()->get('title'); + if($title != '') { + return $title; + } else { + $title->value = $this->uid(); + return $title; + } + } + + /** + * Get formatted date fields + * + * @param string $format + * @param string $field + * @return mixed + */ + public function date($format = null, $field = 'date') { + + if($timestamp = strtotime($this->content()->$field())) { + if(is_null($format)) { + return $timestamp; + } else { + return $this->kirby->options['date.handler']($format, $timestamp); + } + } else { + return false; + } + + } + + /** + * Returns a unique hashed version of the uri, + * which is used for the tinyurl for example + * + * @return string + */ + public function hash() { + if(isset($this->cache['hash'])) return $this->cache['hash']; + + // add a unique hash + $checksum = sprintf('%u', crc32($this->uri())); + return $this->cache['hash'] = base_convert($checksum, 10, 36); + } + + /** + * Magic getter for all content fields + * + * @return Field + */ + public function __call($key, $arguments = null) { + if(isset($this->$key)) { + return $this->$key; + } else if(isset(static::$methods[$key])) { + if(!$arguments) $arguments = array(); + array_unshift($arguments, clone $this); + return call(static::$methods[$key], $arguments); + } else { + return $this->content()->get($key, $arguments); + } + } + + /** + * Alternative for $this->equals() + */ + public function is(Page $page) { + return $this->id() == $page->id(); + } + + /** + * Alternative for $this->is() + */ + public function equals(Page $page) { + return $this->is($page); + } + + /** + * Checks if this page object is the main site + * + * @return boolean + */ + public function isSite() { + return false; + } + + /** + * Checks if this is the active page + * + * @return boolean + */ + public function isActive() { + return $this->site->page()->is($this); + } + + /** + * Checks if the page is open + * + * @return boolean + */ + public function isOpen() { + if($this->isActive()) return true; + return $this->site->page()->parents()->has($this); + } + + /** + * Checks if the page is visible + * + * @return boolean + */ + public function isVisible() { + return !is_null($this->num); + } + + /** + * Checks if the page is invisible + * + * @return boolean + */ + public function isInvisible() { + return !$this->isVisible(); + } + + /** + * Checks if this page is the home page + * You can define the uri of the homepage in your config + * file with the home option. By default it's assumed + * that the homepage folder has the name "home" + * + * @return boolean + */ + public function isHomePage() { + return $this->uri === $this->kirby->options['home']; + } + + /** + * Checks if this page is the error page + * You can define the uri of the error page in your config + * file with the error option. By default it's assumed + * that the error page folder has the name "error" + * + * @return boolean + */ + public function isErrorPage() { + return $this->uri === $this->kirby->options['error']; + } + + /** + * Checks if the page is a child of the given page + * + * @param object Page the page object to check + * @return boolean + */ + public function isChildOf(Page $page) { + return $this->is($page) ? false : $this->parent->is($page); + } + + /** + * Checks if the page is the parent of the given page + * + * @param object Page the page object to check + * @return boolean + */ + public function isParentOf(Page $page) { + return $this->is($page) ? false : $page->parent->is($this); + } + + /** + * Checks if the page is a descendant of the given page + * + * @param object Page the page object to check + * @return boolean + */ + public function isDescendantOf(Page $page) { + return $this->is($page) ? false : $this->parents()->has($page); + } + + /** + * Checks if the page is a descendant of the currently active page + * + * @return boolean + */ + public function isDescendantOfActive() { + return $this->isDescendantOf($this->site->page()); + } + + /** + * Checks if the page is an ancestor of the given page + * + * @param object Page the page object to check + * @return boolean + */ + public function isAncestorOf($page) { + return $page->isDescendantOf($this); + } + + /** + * Checks if the page or any of its files are writable + * + * @return boolean + */ + public function isWritable() { + + $folder = new Folder($this->root()); + + if(!$folder->isWritable()) return false; + + foreach($folder->files() as $f) { + if(!$f->isWritable()) return false; + } + + return true; + + } + + /** + * Returns the timestamp when the page + * has been modified + * + * @return int + */ + public function modified($format = null, $handler = null) { + return f::modified($this->root, $format, $handler ? $handler : $this->kirby->options['date.handler']); + } + + /** + * Returns the index starting from this page + * + * @return Children + */ + public function index() { + return $this->children()->index(); + } + + /** + * Search in subpages and all descendants of this page + * + * @param string $query + * @param array $params + * @return Children + */ + public function search($query, $params = array()) { + return $this->children()->index()->search($query, $params); + } + + // template stuff + + /** + * Returns the name of the used template + * The name of the template is defined by the name + * of the content text file. + * + * i.e. text file: project.txt / template name: project + * + * This method returns the name of the default template + * if there's no template with such a name + * + * @return string + */ + public function template() { + + // check for a cached template name + if(isset($this->cache['template'])) return $this->cache['template']; + + // get the template name + $templateName = $this->intendedTemplate(); + + if($this->kirby->registry->get('template', $templateName)) { + return $this->cache['template'] = $templateName; + } else { + return $this->cache['template'] = 'default'; + } + + } + + /** + * Returns the full path to the used template file + * + * @return string + */ + public function templateFile() { + if($template = $this->kirby->registry->get('template', $this->intendedTemplate())) { + return $template; + } else { + return $this->kirby->registry->get('template', 'default'); + } + } + + /** + * Additional data, which will be passed to the template + * + * @return array + */ + public function templateData() { + return array(); + } + + /** + * Returns the name of the content text file / intended template + * So even if there's no such template it will return the intended name. + * + * @return string + */ + public function intendedTemplate() { + if(isset($this->cache['intendedTemplate'])) return $this->cache['intendedTemplate']; + return $this->cache['intendedTemplate'] = $this->content()->exists() ? $this->content()->name() : 'default'; + } + + /** + * Returns the full path to the intended template file + * This template file may not exist. + * + * @return string + */ + public function intendedTemplateFile() { + return $this->kirby->component('template')->file($this->intendedTemplate()); + } + + /** + * Checks if there's a dedicated template for this page + * Will return false when the default template is used + * + * @return boolean + */ + public function hasTemplate() { + return $this->intendedTemplate() == $this->template(); + } + + /** + * Sends all appropriate headers for this page + * Can be configured with the headers config array, + * which should contain all header definitions for each template + */ + public function headers() { + + $template = $this->template(); + if(isset($this->kirby->options['headers'][$template])) { + $headers = $this->kirby->options['headers'][$template]; + + if(is_numeric($headers)) { + header::status($headers); + } else if(is_callable($headers)) { + call($headers, $this); + } + + } else if($this->isErrorPage()) { + header::notfound(); + } + + } + + /** + * Returns the root for the content file + * + * @return string + */ + public function textfile($template = null) { + if(is_null($template)) $template = $this->intendedTemplate(); + return textfile($this->diruri(), $template); + } + + /** + * Private method to create a page directory + */ + static protected function createDirectory($uri) { + + $uid = str::slug(basename($uri)); + $parentURI = dirname($uri); + $parent = ($parentURI == '.' or empty($parentURI) or $parentURI == DS) ? site() : page($parentURI); + + if(!$parent) { + throw new Exception('The parent does not exist'); + } + + // check for an entered sorting number + if(preg_match('!^(\d+)\-(.*)!', $uid, $matches)) { + $num = $matches[1]; + $uid = $matches[2]; + $dir = $num . '-' . $uid; + } else { + $num = false; + $dir = $uid; + } + + // make sure to check a fresh page + $parent->reset(); + + if($parent->children()->findBy('uid', $uid)) { + throw new Exception('The page UID exists'); + } + + if(!dir::make($parent->root() . DS . $dir)) { + throw new Exception('The directory could not be created'); + } + + // make sure the new directory is available everywhere + $parent->reset(); + + return $parent->id() . '/' . $uid; + + } + + /** + * Creates a new page object + * + * @param string $uri + * @param string $template + * @param array $data + */ + static public function create($uri, $template, $data = array()) { + + if(!is_string($template) or empty($template)) { + throw new Exception('Please pass a valid template name as second argument'); + } + + // try to create the new directory + $uri = static::createDirectory($uri); + + // create the path for the textfile + $file = textfile($uri, $template); + + // try to store the data in the text file + if(!data::write($file, $data, 'kd')) { + throw new Exception('The page file could not be created'); + } + + // get the new page object + $page = page($uri); + + if(!is_a($page, 'Page')) { + throw new Exception('The new page object could not be found'); + } + + // let's create a model if one is defined + if(isset(static::$models[$template])) { + $model = static::$models[$template]; + $page = new $model($page->parent(), $page->dirname()); + } + + kirby::instance()->cache()->flush(); + + return $page; + + } + + /** + * Update the page with a new set of data + * + * @param array + */ + public function update($input = array()) { + + $data = a::update($this->content()->toArray(), $input); + + if(!data::write($this->textfile(), $data, 'kd')) { + throw new Exception('The page could not be updated'); + } + + $this->kirby->cache()->flush(); + $this->reset(); + $this->touch(); + return true; + + } + + /** + * Increment a field value by one or a given value + * + * @param string $field + * @param int $by + * @param int $max + * @return Page + */ + public function increment($field, $by = 1, $max = null) { + $this->update(array( + $field => function($value) use($by, $max) { + $new = (int)$value + $by; + return ($max and $new >= $max) ? $max : $new; + } + )); + return $this; + } + + /** + * Decrement a field value by one or a given value + * + * @param string $field + * @param int $by + * @param int $min + * @return Page + */ + public function decrement($field, $by = 1, $min = 0) { + $this->update(array( + $field => function($value) use($by, $min) { + $new = (int)$value - $by; + return $new <= $min ? $min : $new; + } + )); + return $this; + } + + /** + * Changes the uid for the page + * + * @param string $uid + */ + public function move($uid) { + + $uid = str::slug($uid); + + if(empty($uid)) { + throw new Exception('The uid is missing'); + } + + // don't do anything if the uid exists + if($this->uid() === $uid) return true; + + // check for an existing page with the same UID + if($this->siblings()->not($this)->find($uid)) { + throw new Exception('A page with this uid already exists'); + } + + $dir = $this->isVisible() ? $this->num() . '-' . $uid : $uid; + $root = dirname($this->root()) . DS . $dir; + + if(!dir::move($this->root(), $root)) { + throw new Exception('The directory could not be moved'); + } + + $this->dirname = $dir; + $this->root = $root; + $this->uid = $uid; + + // assign a new id and uri + $this->id = $this->uri = ltrim($this->parent->id() . '/' . $this->uid, '/'); + + // clean the cache + $this->kirby->cache()->flush(); + $this->reset(); + return true; + + } + + /** + * Return the prepended number for the page + * or changes it to the number passed as parameter + */ + public function sort($num = null) { + + if(!$num and $num !== 0) return $this->num(); + if($num === $this->num()) return true; + + $dir = $num . '-' . $this->uid(); + $root = dirname($this->root()) . DS . $dir; + + if(!dir::move($this->root(), $root)) { + throw new Exception('The directory could not be moved'); + } + + $this->dirname = $dir; + $this->num = $num; + $this->root = $root; + $this->kirby->cache()->flush(); + $this->reset(); + return true; + + } + + /** + * Make the page invisible by removing the prepended number + */ + public function hide() { + + if($this->isInvisible()) return true; + + $root = dirname($this->root()) . DS . $this->uid(); + + if(!dir::move($this->root(), $root)) { + throw new Exception('The directory could not be moved'); + } + + $this->dirname = $this->uid(); + $this->num = null; + $this->root = $root; + $this->kirby->cache()->flush(); + $this->reset(); + return true; + + } + + public function isDeletable() { + + if($this->isSite()) return false; + if($this->isHomePage()) return false; + if($this->isErrorPage()) return false; + + return true; + + } + + /** + * Deletes the page + * + * @param boolean $force Forces the page to be deleted even if there are subpages + */ + public function delete($force = false) { + + if(!$this->isDeletable()) { + throw new Exception('The page cannot be deleted'); + } + + if($force === false and $this->children()->count()) { + throw new Exception('This page has subpages'); + } + + $parent = $this->parent(); + + if(!dir::remove($this->root())) { + throw new Exception('The page could not be deleted'); + } + + $this->kirby->cache()->flush(); + $parent->reset(); + return true; + + } + + /** + * Converts the entire page object into + * a plain PHP array + * + * @param closure $callback Filter callback + * @return array + */ + public function toArray($callback = null) { + + $data = array( + 'id' => $this->id(), + 'title' => $this->title()->toString(), + 'parent' => $this->parent()->uri(), + 'dirname' => $this->dirname(), + 'diruri' => $this->diruri(), + 'url' => $this->url(), + 'contentUrl' => $this->contentUrl(), + 'tinyUrl' => $this->tinyUrl(), + 'depth' => $this->depth(), + 'uri' => $this->uri(), + 'root' => $this->root(), + 'uid' => $this->uid(), + 'slug' => $this->slug(), + 'num' => $this->num(), + 'hash' => $this->hash(), + 'modified' => $this->modified(), + 'template' => $this->template(), + 'intendedTemplate' => $this->intendedTemplate(), + 'content' => $this->content()->toArray(), + ); + + if(is_null($callback)) { + return $data; + } else if(is_callable($callback)) { + return $callback($this); + } + + } + + /** + * Tries to find a controller for + * the current page and loads the data + * + * @return array + */ + public function controller($arguments = array()) { + + $controller = $this->kirby->registry->get('controller', $this->template()); + + if(is_a($controller, 'Closure')) { + return (array)call_user_func_array($controller, array( + $this->site, + $this->site->children(), + $this, + $arguments + )); + } + + return array(); + + } + + /** + * Converts the entire page array into + * a json string + * + * @param closure $callback Filter callback + * @return string + */ + public function toJson($callback = null) { + return json_encode($this->toArray($callback)); + } + + /** + * Makes it possible to echo the entire object + * + * @return string + */ + public function __toString() { + return (string)$this->id(); + } + +} diff --git a/kirby/core/pages.php b/kirby/core/pages.php new file mode 100644 index 0000000..2095097 --- /dev/null +++ b/kirby/core/pages.php @@ -0,0 +1,330 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class PagesAbstract extends Collection { + + static public $methods = array(); + + /** + * Constructor + */ + public function __construct($data = array()) { + foreach($data as $object) { + $this->add($object); + } + } + + public function __call($method, $arguments) { + + if(isset(static::$methods[$method])) { + array_unshift($arguments, clone $this); + return call(static::$methods[$method], $arguments); + } else { + return $this->get($method); + } + + } + + /** + * Adds a single page object to the + * collection by id or the entire object + * + * @param mixed $page + */ + public function add($page) { + + if(is_a($page, 'Collection')) { + foreach($page as $object) $this->add($object); + } else if(is_string($page) and $object = page($page)) { + $this->data[$object->id()] = $object; + } else if(is_a($page, 'Page')) { + $this->data[$page->id()] = $page; + } + + return $this; + + } + + /** + * Returns a new collection of pages without the given pages + * + * @param args any number of uris or page elements, passed as individual arguments + * @return object a new collection without the pages + */ + public function not() { + $collection = clone $this; + foreach(func_get_args() as $uri) { + if(is_array($uri) or $uri instanceof Traversable) { + foreach($uri as $u) { + $collection = $collection->not($u); + } + } else if(is_a($uri, 'Page')) { + // unset by Page object + unset($collection->data[$uri->id()]); + } else if(isset($collection->data[$uri])) { + // unset by URI + unset($collection->data[$uri]); + } else if($page = $collection->findBy('uid', $uri)) { + // unset by UID + unset($collection->data[$page->id()]); + } + } + return $collection; + } + + public function find() { + + $args = func_get_args(); + + if(!count($args)) { + return false; + } + + if(count($args) === 1 and is_array($args[0])) { + $args = $args[0]; + } + + if(count($args) > 1) { + $pages = new static(); + foreach($args as $id) { + if($page = $this->find($id)) { + $pages->data[$page->id()] = $page; + } + } + return $pages; + } else { + + // get the first argument and remove slashes + $id = trim($args[0], '/'); + + // fast access to direct uris + return isset($this->data[$id]) ? $this->data[$id] : null; + + } + + } + + /** + * Find a single page by a given value + * + * @param string $field + * @param string $value + * @return Page + */ + public function findBy($field, $value) { + foreach($this->data as $page) { + if($page->$field() == $value) return $page; + } + return false; + } + + /** + * Find the open page in a set + * + * @return Page + */ + public function findOpen() { + return $this->findBy('isOpen', true); + } + + /** + * Filters the collection by visible pages + * + * @return Children + */ + public function visible() { + $collection = clone $this; + return $collection->filterBy('isVisible', true); + } + + /** + * Filters the collection by invisible pages + * + * @return Children + */ + public function invisible() { + $collection = clone $this; + return $collection->filterBy('isInvisible', true); + } + + /** + * Checks if a page is in a set of children + * + * @param Page | string $page + * @return boolean + */ + public function has($page) { + $uri = is_string($page) ? $page : $page->id(); + return parent::has($uri); + } + + /** + * Native search method to search for anything within the collection + */ + public function search($query, $params = array()) { + + if(is_string($params)) { + $params = array('fields' => str::split($params, '|')); + } + + $defaults = array( + 'minlength' => 2, + 'fields' => array(), + 'words' => false, + 'score' => array() + ); + + $options = array_merge($defaults, $params); + $collection = clone $this; + $searchwords = preg_replace('/(\s)/u',',', $query); + $searchwords = str::split($searchwords, ',', $options['minlength']); + + if(!empty($options['stopwords'])) { + $searchwords = array_diff($searchwords, $options['stopwords']); + } + + if(empty($searchwords)) return $collection->limit(0); + + $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($page) use($query, $searchwords, $preg, $options) { + + $data = $page->content()->toArray(); + $keys = array_keys($data); + + if(!empty($options['fields'])) { + $keys = array_intersect($keys, $options['fields']); + } + + $page->searchHits = 0; + $page->searchScore = 0; + + foreach($keys as $key) { + + $score = a::get($options['score'], $key, 1); + + // check for a match + if($matches = preg_match_all($preg, $data[$key], $r)) { + + $page->searchHits += $matches; + $page->searchScore += $matches * $score; + + // check for full matches + if($matches = preg_match_all('!' . preg_quote($query) . '!i', $data[$key], $r)) { + $page->searchScore += $matches * $score; + } + + } + + } + + return $page->searchHits > 0 ? true : false; + + }); + + $results = $results->sortBy('searchScore', SORT_DESC); + + return $results; + + } + + /** + * Returns files from all pages + * + * @return object A collection of all files of the pages (not of their subpages) + */ + public function files() { + + $files = new Collection(); + + foreach($this->data as $page) { + foreach($page->files() as $file) { + $files->append($page->id() . '/' . strtolower($file->filename()), $file); + } + } + + return $files; + + } + + // File type filters + public function images() { return $this->files()->filterBy('type', 'image'); } + public function videos() { return $this->files()->filterBy('type', 'video'); } + public function documents() { return $this->files()->filterBy('type', 'document'); } + public function audio() { return $this->files()->filterBy('type', 'audio'); } + public function code() { return $this->files()->filterBy('type', 'code'); } + public function archives() { return $this->files()->filterBy('type', 'archive'); } + + /** + * Groups the pages by a given field + * + * @param string $field + * @param bool $i (ignore upper/lowercase for group names) + * @return object A collection with an item for each group and a Pages object for each group + */ + public function groupBy($field, $i = true) { + + $groups = array(); + + foreach($this->data as $key => $item) { + + $value = $item->content()->get($field)->value(); + + // make sure that there's always a proper value to group by + if(!$value) throw new Exception('Invalid grouping value for key: ' . $key); + + // ignore upper/lowercase for group names + if($i) $value = str::lower($value); + + if(!isset($groups[$value])) { + // create a new entry for the group if it does not exist yet + $groups[$value] = new Pages(array($key => $item)); + } else { + // add the item to an existing group + $groups[$value]->set($key, $item); + } + + } + + return new Collection($groups); + + } + + /** + * Converts the pages collection + * into a plain array + * + * @param closure $callback Filter callback for each item + * @return array + */ + public function toArray($callback = null) { + $data = array(); + foreach($this as $page) { + $data[] = is_string($page) ? $page : $page->toArray($callback); + } + return $data; + } + + /** + * Converts the pages collection + * into a json string + * + * @param closure $callback Filter callback for each item + * @return string + */ + public function toJson($callback = null) { + return json_encode($this->toArray($callback)); + } + +} \ No newline at end of file diff --git a/kirby/core/role.php b/kirby/core/role.php new file mode 100644 index 0000000..bbfb897 --- /dev/null +++ b/kirby/core/role.php @@ -0,0 +1,102 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class RoleAbstract { + + protected $id = null; + protected $name = null; + protected $panel = false; + protected $permissions = array( + 'panel.access' => true, + 'panel.site.update' => true, + 'panel.page.create' => true, + 'panel.page.update' => true, + 'panel.page.move' => true, + 'panel.page.sort' => true, + 'panel.page.hide' => true, + 'panel.page.delete' => true, + 'panel.file.upload' => true, + 'panel.file.replace' => true, + 'panel.file.update' => true, + 'panel.file.delete' => true, + 'panel.user.add' => true, + 'panel.user.edit' => true, + 'panel.user.role' => true, + 'panel.user.delete' => true, + ); + + public $default = false; + + public function __construct($data = array()) { + + if(!isset($data['id'])) throw new Exception('The role id is missing'); + if(!isset($data['name'])) throw new Exception('The role name is missing'); + + // required data + $this->id = $data['id']; + $this->name = $data['name']; + + if(isset($data['permissions']) and is_array($data['permissions'])) { + $this->permissions = a::merge($this->permissions, $data['permissions']); + } else if(isset($data['permissions']) and $data['permissions'] === false) { + $this->permissions = array_fill_keys(array_keys($this->permissions), false); + } else { + $this->permissions = $this->permissions; + } + + // fallback permissions support for old 'panel' role variable + if(isset($data['panel']) and is_bool($data['panel'])) { + $this->permissions['panel.access'] = $data['panel']; + } + + // is this role the default role? + if(isset($data['default'])) { + $this->default = $data['default'] === true; + } + + } + + public function id() { + return $this->id; + } + + public function name() { + return $this->name; + } + + // support for old 'panel' role permission + public function hasPanelAccess() { + return $this->hasPermission('panel.access'); + } + + public function hasPermission($target) { + if($this->id == 'admin') { + return true; + } else if(isset($this->permissions[$target]) and $this->permissions[$target] === true) { + return true; + } else { + return false; + } + } + + public function isDefault() { + return $this->default; + } + + public function users() { + return kirby::instance()->site()->users()->filterBy('role', $this->id); + } + + public function __toString() { + return (string)$this->id; + } + +} diff --git a/kirby/core/roles.php b/kirby/core/roles.php new file mode 100644 index 0000000..2d7ff69 --- /dev/null +++ b/kirby/core/roles.php @@ -0,0 +1,84 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class RolesAbstract extends Collection { + + // cache for the default role + protected $default = null; + + /** + * Constructor + */ + public function __construct() { + + $roles = kirby::instance()->option('roles'); + + // set the default set of roles, if roles are not configured + if(empty($roles)) { + $roles = array( + array( + 'id' => 'admin', + 'name' => 'Admin', + 'default' => true + ), + array( + 'id' => 'editor', + 'name' => 'Editor', + 'permissions' => array( + 'panel.access' => true, + 'panel.site.update' => false, + 'panel.page.create' => true, + 'panel.page.update' => true, + 'panel.page.move' => true, + 'panel.page.sort' => true, + 'panel.page.hide' => true, + 'panel.page.delete' => true, + 'panel.file.upload' => true, + 'panel.file.replace' => true, + 'panel.file.update' => true, + 'panel.file.delete' => true, + 'panel.user.add' => false, + 'panel.user.edit' => false, + 'panel.user.role' => false, + 'panel.user.delete' => false + ) + ) + ); + } + + foreach($roles as $role) { + $role = new Role($role); + $this->data[$role->id()] = $role; + } + + // check for a valid admin role + if(!isset($this->data['admin'])) { + throw new Exception('There must be an admin role'); + } + + // check for a valid default role + if(!$this->findDefault()) { + $this->data['admin']->default = true; + } + + } + + /** + * Returns the default role for new users + * + * @return Role + */ + public function findDefault() { + if(!is_null($this->default)) return $this->default; + return $this->default = $this->findBy('isDefault', true); + } + +} diff --git a/kirby/core/site.php b/kirby/core/site.php new file mode 100644 index 0000000..1180471 --- /dev/null +++ b/kirby/core/site.php @@ -0,0 +1,337 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class SiteAbstract extends Page { + + // the current page + public $page = null; + + /** + * Constructor + * + */ + public function __construct(Kirby $kirby) { + + $this->kirby = $kirby; + $this->url = $kirby->urls()->index(); + $this->depth = 0; + $this->uri = ''; + $this->site = $this; + $this->page = null; + + // build ugly urls if rewriting is disabled + if($this->kirby->options['rewrite'] === false) { + $this->url .= '/index.php'; + } + + $this->root = $kirby->roots()->content(); + $this->dirname = basename($this->root); + + } + + /** + * Cleans the temporary internal cache + */ + public function reset() { + $this->cache = array(); + } + + /** + * The id is an empty string in case of the site object + * + * @return string + */ + public function id() { + return ''; + } + + /** + * The base diruri is bascially just an empty string + * + * @return string + */ + public function diruri() { + return ''; + } + + /** + * Returns the base url for the site + * + * @return string + */ + public function url() { + return $this->url; + } + + /** + * Returns the full URL for the content folder + * + * @return string + */ + public function contentUrl() { + return $this->kirby()->urls()->content(); + } + + /** + * Checks if this object is the main site + * + * @return boolean + */ + public function isSite() { + return true; + } + + /** + * Returns the usable template + * + * @return string + */ + public function template() { + return 'site'; + } + + /** + * The site has no template + * + * @return boolean + */ + public function templateFile() { + return false; + } + + /** + * Returns the intended template + * + * @return string + */ + public function intendedTemplate() { + return 'site'; + } + + /** + * Again, the site has no template! + * + * @return boolean + */ + public function intendedTemplateFile() { + return false; + } + + /** + * There can't be a template for the site + * Didn't you still get it yet? + * + * @return boolean + */ + public function hasTemplate() { + return false; + } + + /** + * Sets the currently active page + * and returns its page object + * + * @param string $uri + * @return Page + */ + public function visit($uri = '') { + + $uri = trim($uri, '/'); + + if(empty($uri)) { + return $this->page = $this->homePage(); + } else { + if($page = $this->children()->find($uri)) { + return $this->page = $page; + } else { + return $this->page = $this->errorPage(); + } + } + + } + + /** + * Returns the currently active page or any other page by uri + * + * @param string $uri Optional uri to get any page on the site + * @return Page + */ + public function page($uri = null) { + if(is_null($uri)) { + return is_null($this->page) ? $this->page = $this->homePage() : $this->page; + } else { + return $this->children()->find($uri); + } + } + + /** + * Alternative for $this->children() + * + * @return Children + */ + public function pages() { + return $this->children(); + } + + /** + * Builds a breadcrumb collection + * + * @return Children + */ + public function breadcrumb() { + + if(isset($this->cache['breadcrumb'])) return $this->cache['breadcrumb']; + + // get all parents and flip the order + $crumb = $this->page()->parents()->flip(); + + // add the home page + $crumb->prepend($this->homePage()->uri(), $this->homePage()); + + // add the active page + $crumb->append($this->page()->uri(), $this->page()); + + return $this->cache['breadcrumb'] = $crumb; + + } + + /** + * Alternative for $this->page() + * + * @return Page + */ + public function activePage() { + return $this->page(); + } + + /** + * Returns the error page object + * + * @return Page + */ + public function errorPage() { + if(isset($this->cache['errorPage'])) return $this->cache['errorPage']; + return $this->cache['errorPage'] = $this->children()->find($this->kirby->options['error']); + } + + /** + * Returns the home page object + * + * @return Page + */ + public function homePage() { + if(isset($this->cache['homePage'])) return $this->cache['homePage']; + return $this->cache['homePage'] = $this->children()->find($this->kirby->options['home']); + } + + /** + * Returns the locale for the site + * + * @return string + */ + public function locale() { + return isset($this->kirby->options['locale']) ? $this->kirby->options['locale'] : 'en_US'; + } + + /** + * Checks if the site is a multi language site + * + * @return boolean + */ + public function multilang() { + return false; + } + + /** + * Placeholder for multilanguage sites + */ + public function languages() { + return null; + } + + /** + * Placeholder for multilanguage sites + */ + public function language() { + return null; + } + + /** + * Placeholder for multilanguage sites + */ + public function defaultLanguage() { + return null; + } + + /** + * Return the detected language + */ + public function detectedLanguage() { + return null; + } + + /** + * Returns a collection of all users + * + * @return Users + */ + public function users() { + return new Users(); + } + + /** + * Returns the current user + * + * @param string $username Optional way to search for a single user + * @return User + */ + public function user($username = null) { + if(is_null($username)) return User::current(); + try { + return new User($username); + } catch(Exception $e) { + return null; + } + } + + /** + * Returns a collection of all roles + * + * @return Roles + */ + public function roles() { + return new Roles(); + } + + /** + * Gets the last modification date of all pages + * in the content folder. + * + * @param mixed $format + * @param mixed $handler + * @return mixed + */ + public function modified($format = null, $handler = null) { + return dir::modified($this->root, $format, $handler ? $handler : $this->kirby->options['date.handler']); + } + + /** + * 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 boolean + */ + public function wasModifiedAfter($time) { + return dir::wasModifiedAfter($this->root(), $time); + } + +} \ No newline at end of file diff --git a/kirby/core/user.php b/kirby/core/user.php new file mode 100644 index 0000000..f004a14 --- /dev/null +++ b/kirby/core/user.php @@ -0,0 +1,349 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class UserAbstract { + + protected $username = null; + protected $cache = array(); + protected $data = null; + + public function __construct($username) { + + $this->username = str::slug(basename($username)); + + // check if the account file exists + if(!file_exists($this->file())) { + throw new Exception('The user account could not be found'); + } + + } + + /** + * Returns the username + * + * @return string + */ + public function username() { + return $this->username; + } + + /** + * get all data for the user + */ + public function data() { + + if(!is_null($this->data)) return $this->data; + + // get all data from the account file + $this->data = data::read($this->file(), 'yaml'); + + // make sure all keys are lowercase + $this->data = array_change_key_case($this->data, CASE_LOWER); + + // remove garbage + unset($this->data[0]); + + // add the username + $this->data['username'] = $this->username; + + // return the data array + return $this->data; + + } + + public function __get($key) { + return a::get($this->data(), strtolower($key)); + } + + public function __call($key, $arguments = null) { + return $this->__get($key); + } + + public function role() { + + $roles = kirby::instance()->site()->roles(); + $data = $this->data(); + + if(empty($data['role'])) { + // apply the default role, if no role is stored for the user + $data['role'] = $roles->findDefault()->id(); + } + + // return the role by id + if($role = $roles->get($data['role'])) { + return $role; + } else { + return $roles->findDefault(); + } + + } + + public function hasRole() { + $roles = func_get_args(); + return in_array($this->role()->id(), $roles); + } + + // support for old 'panel' role permission + public function hasPanelAccess() { + return $this->role()->hasPermission('panel.access'); + } + + public function hasPermission($target) { + return $this->role()->hasPermission($target); + } + + public function isAdmin() { + return $this->role()->id() == 'admin'; + } + + public function avatar() { + + if(isset($this->cache['avatar'])) return $this->cache['avatar']; + + $avatar = new Avatar($this); + + return $this->cache['avatar'] = $avatar->exists() ? $avatar : false; + + } + + public function avatarRoot($extension = 'jpg') { + return kirby::instance()->roots()->avatars() . DS . $this->username() . '.' . $extension; + } + + public function gravatar($size = 256) { + return gravatar($this->email(), $size); + } + + protected function file() { + return kirby::instance()->roots()->accounts() . DS . $this->username() . '.php'; + } + + public function textfile() { + return $this->file(); + } + + public function exists() { + return file_exists($this->file()); + } + + public function generateKey() { + return str::random(64); + } + + public function generateSecret($key) { + return sha1($this->username() . $key); + } + + public function login($password) { + + static::logout(); + + if(!password::match($password, $this->password)) return false; + + // create a new session id + s::regenerateId(); + + $key = $this->generateKey(); + $secret = $this->generateSecret($key); + + s::set('kirby_auth_secret', $secret); + s::set('kirby_auth_username', $this->username()); + + cookie::set( + s::$name . '_auth', + $key, + s::$cookie['lifetime'], + s::$cookie['path'], + s::$cookie['domain'], + s::$cookie['secure'], + s::$cookie['httponly'] + ); + + return true; + + } + + static public function logout() { + s::destroy(); + cookie::remove(s::$name . '_auth'); + } + + public function is($user) { + if(!is_a($user, 'User')) return false; + return $this->username() === $user->username(); + } + + public function isCurrent() { + return $this->is(static::current()); + } + + static public function validate($data = array(), $mode = 'insert') { + + if($mode == 'insert') { + + if(empty($data['username'])) { + throw new Exception('Invalid username'); + } + + if(empty($data['password'])) { + throw new Exception('Invalid password'); + } + + } + + if(!empty($data['email']) and !v::email($data['email'])) { + throw new Exception('Invalid email'); + } + + } + + public function update($data = array()) { + + // sanitize the given data + $data = $this->sanitize($data, 'update'); + + // validate the updated dataset + $this->validate($data, 'update'); + + // don't update the username + unset($data['username']); + + // create a new hash for the password + if(!empty($data['password'])) { + $data['password'] = password::hash($data['password']); + } + + // merge with existing fields + $this->data = array_merge($this->data(), $data); + + foreach($this->data as $key => $value) { + if(is_null($value)) unset($this->data[$key]); + } + + // save the new user data + static::save($this->file(), $this->data); + + // return the updated user project + return $this; + + } + + public function delete() { + + if($avatar = $this->avatar()) { + $avatar->delete(); + } + + if(!f::remove($this->file())) { + throw new Exception('The account could not be deleted'); + } else { + return true; + } + + } + + static public function sanitize($data, $mode = 'insert') { + + // all usernames must be lowercase + $data['username'] = str::slug(a::get($data, 'username')); + + // convert all keys to lowercase + $data = array_change_key_case($data, CASE_LOWER); + + // return the cleaned up data + return $data; + + } + + /** + * Creates a new user + * + * @param array $user + * @return User + */ + static public function create($data = array()) { + + // sanitize the given data for the new user + $data = static::sanitize($data, 'insert'); + + // validate the dataset + static::validate($data, 'insert'); + + // create the file root + $file = kirby::instance()->roots()->accounts() . DS . $data['username'] . '.php'; + + // check for an existing username + if(file_exists($file)) { + throw new Exception('The username is taken'); + } + + // create a new hash for the password + if(!empty($data['password'])) { + $data['password'] = password::hash($data['password']); + } + + static::save($file, $data); + + // return the created user project + return new static($data['username']); + + } + + static protected function save($file, $data) { + + $yaml = '' . PHP_EOL . PHP_EOL; + $yaml .= data::encode($data, 'yaml'); + + if(!f::write($file, $yaml)) { + throw new Exception('The user account could not be saved'); + } else { + return true; + } + + } + + static public function unauthorize() { + s::remove('kirby_auth_secret'); + s::remove('kirby_auth_username'); + cookie::remove('kirby_auth'); + } + + static public function current() { + + $cookey = cookie::get(s::$name . '_auth'); + $username = s::get('kirby_auth_username'); + + if(empty($cookey)) { + static::unauthorize(); + return false; + } + + if(s::get('kirby_auth_secret') !== sha1($username . $cookey)) { + static::unauthorize(); + return false; + } + + // find the logged in user by token + try { + $user = new static($username); + return $user; + } catch(Exception $e) { + static::unauthorize(); + return false; + } + + } + + public function __toString() { + return (string)$this->username; + } + +} \ No newline at end of file diff --git a/kirby/core/users.php b/kirby/core/users.php new file mode 100644 index 0000000..f0e1f31 --- /dev/null +++ b/kirby/core/users.php @@ -0,0 +1,38 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class UsersAbstract extends Collection { + + public function __construct() { + + $root = kirby::instance()->roots()->accounts(); + + foreach(dir::read($root) as $file) { + + // skip invalid account files + if(f::extension($file) != 'php') continue; + + $user = new User(f::name($file)); + $this->append($user->username(), $user); + + } + + } + + public function create($data) { + return user::create($data); + } + + public function find($username) { + return $this->findBy('username', $username); + } + +} \ No newline at end of file diff --git a/kirby/extensions/methods.php b/kirby/extensions/methods.php new file mode 100644 index 0000000..33034f4 --- /dev/null +++ b/kirby/extensions/methods.php @@ -0,0 +1,279 @@ +value = html($field->value, $keepTags); + return $field; +}; + +/** + * Escapes unwanted characters in the field value + * to protect from possible xss attacks or other + * unwanted side effects in your html code + * @param Field $field The calling Kirby Field instance + * @param string $context html|attr|css|js|url + * @return Field + */ +field::$methods['escape'] = field::$methods['esc'] = function($field, $context = 'html') { + $field->value = esc($field->value, $context); + return $field; +}; + +/** + * Converts html entities and specialchars in the field + * value to valid xml entities + * @param Field $field The calling Kirby Field instance + * @return Field + */ +field::$methods['xml'] = field::$methods['x'] = function($field) { + $field->value = xml($field->value); + return $field; +}; + +/** + * Parses the field value as kirbytext + * @param Field $field The calling Kirby Field instance + * @return Field + */ +field::$methods['kirbytext'] = field::$methods['kt'] = function($field) { + $field->value = kirbytext($field); + return $field; +}; + +/** + * Parses the field value as markdown + * @param Field $field The calling Kirby Field instance + * @return Field + */ +field::$methods['markdown'] = field::$methods['md'] = function($field) { + $field->value = markdown($field->value); + return $field; +}; + +/** + * Converts the field value to lower case + * @param Field $field The calling Kirby Field instance + * @return Field + */ +field::$methods['lower'] = function($field) { + $field->value = str::lower($field->value); + return $field; +}; + +/** + * Converts the field value to upper case + * @param Field $field The calling Kirby Field instance + * @return Field + */ +field::$methods['upper'] = function($field) { + $field->value = str::upper($field->value); + return $field; +}; + +/** + * Applies the widont rule to avoid single + * words on the last line + * @param Field $field The calling Kirby Field instance + * @return Field + */ +field::$methods['widont'] = function($field) { + $field->value = widont($field->value); + return $field; +}; + +/** + * Creates a simple text excerpt without formats + * @param Field $field The calling Kirby Field instance + * @param integer $chars The desired excerpt length + * @return string + */ +field::$methods['excerpt'] = function($field, $chars = 140, $mode = 'chars') { + return excerpt($field, $chars, $mode); +}; + +/** + * Shortens the field value by the given length + * @param Field $field The calling Kirby Field instance + * @param integer $length The desired string length + * @param string $rep The attached ellipsis character if the string is longer + * @return string + */ +field::$methods['short'] = function($field, $length, $rep = '…') { + return str::short($field->value, $length, $rep); +}; + +/** + * Returns the string length of the field value + * @param Field $field The calling Kirby Field instance + * @return integer + */ +field::$methods['length'] = function($field) { + return str::length($field->value); +}; + +/** + * Returns the word count for the field value + * @param Field $field The calling Kirby Field instance + * @return integer + */ +field::$methods['words'] = function($field) { + return str_word_count(strip_tags($field->value)); +}; + +/** + * Splits the field value by the given separator + * @param Field $field The calling Kirby Field instance + * @param string $separator The string to split the field value by + * @return array + */ +field::$methods['split'] = function($field, $separator = ',') { + return str::split($field->value, $separator); +}; + +/** + * Parses the field value as yaml and returns an array + * @param Field $field The calling Kirby Field instance + * @return array + */ +field::$methods['yaml'] = function($field) { + return yaml($field->value); +}; + +/** + * Checks if the field value is empty + * @param Field $field The calling Kirby Field instance + * @return boolean + */ +field::$methods['empty'] = field::$methods['isEmpty'] = function($field) { + return empty($field->value); +}; + +/** + * Checks if the field value is not empty + * @param Field $field The calling Kirby Field instance + * @return boolean + */ +field::$methods['isNotEmpty'] = function($field) { + return !$field->isEmpty(); +}; + +/** + * Returns a page object from a uri in a field + * @param Field $field The calling Kirby Field instance + * @return Collection + */ +field::$methods['toPage'] = function($field) { + return page($field->value); +}; + +/** + * Returns all page objects from a yaml list or a $sep separated string in a field + * @param Field $field The calling Kirby Field instance + * @return Collection + */ +field::$methods['pages'] = field::$methods['toPages'] = function($field, $sep = null) { + + if($sep !== null) { + $array = $field->split($sep); + } else { + $array = $field->yaml(); + } + + return $field->site()->pages()->find($array); + +}; + +/** + * Returns a file object from a filename in a field + * @param Field $field The calling Kirby Field instance + * @return Collection + */ +field::$methods['toFile'] = function($field) { + return $field->page()->file($field->value); +}; + +/** + * Adds 'or' method to Field objects, which allows getting a field + * value or getting back a default value if the field is empty. + * @author fvsch + * @param Field $field The calling Kirby Field instance + * @param mixed $fallback Fallback value returned if field is empty + * @return mixed + */ +field::$methods['or'] = function($field, $fallback = null) { + return $field->empty() ? $fallback : $field; +}; + +/** + * Filter the Field value, or a fallback value if the Field is empty, + * to get a boolean value. '1', 'on', 'true' or 'yes' will be true, + * and everything else will be false. + * @author fvsch + * @param Field $field The calling Kirby Field instance + * @param boolean $default Default value returned if field is empty + * @return boolean + */ +field::$methods['bool'] = field::$methods['isTrue'] = function($field, $default = false) { + $val = $field->empty() ? $default : $field->value; + return filter_var($val, FILTER_VALIDATE_BOOLEAN); +}; + +/** + * Checks if the field content is false + * @param Field $field The calling Kirby Field instance + * @return boolean + */ +field::$methods['isFalse'] = function($field) { + return !$field->bool(); +}; + +/** + * Get an integer value for the Field. + * @author fvsch + * @param Object(Field) [$field] The calling Kirby Field instance + * @param integer [$default] Default value returned if field is empty + * @return integer + */ +field::$methods['int'] = function($field, $default = 0) { + $val = $field->empty() ? $default : $field->value; + return intval($val); +}; + +/** + * Get a float value for the Field + * @param Field $field The calling Kirby Field instance + * @param int $default Default value returned if field is empty + * @return float + */ +field::$methods['float'] = function($field, $default = 0) { + $val = $field->empty() ? $default : $field->value; + return floatval($val); +}; + +field::$methods['toStructure'] = field::$methods['structure'] = function($field) { + return structure($field->yaml(), $field->page()); +}; + +field::$methods['link'] = function($field, $attr1 = array(), $attr2 = array()) { + $a = new Brick('a', $field->value()); + + if(is_string($attr1)) { + $a->attr('href', url($attr1)); + $a->attr($attr2); + } else { + $a->attr('href', $field->page()->url()); + $a->attr($attr1); + } + + return $a; + +}; + +field::$methods['toUrl'] = field::$methods['url'] = function($field) { + return url($field->value()); +}; \ No newline at end of file diff --git a/kirby/extensions/tags.php b/kirby/extensions/tags.php new file mode 100644 index 0000000..3c96550 --- /dev/null +++ b/kirby/extensions/tags.php @@ -0,0 +1,309 @@ + array(), + 'html' => function($tag) { + return strtolower($tag->attr('date')) == 'year' ? date('Y') : date($tag->attr('date')); + } +); + +// email tag +kirbytext::$tags['email'] = array( + 'attr' => array( + 'class', + 'title', + 'text', + 'rel' + ), + 'html' => function($tag) { + return html::email($tag->attr('email'), html($tag->attr('text')), array( + 'class' => $tag->attr('class'), + 'title' => $tag->attr('title'), + 'rel' => $tag->attr('rel'), + )); + } +); + +// file tag +kirbytext::$tags['file'] = array( + 'attr' => array( + 'text', + 'class', + 'title', + 'rel', + 'target', + 'popup' + ), + 'html' => function($tag) { + + // build a proper link to the file + $file = $tag->file($tag->attr('file')); + $text = $tag->attr('text'); + + if(!$file) return $text; + + // use filename if the text is empty and make sure to + // ignore markdown italic underscores in filenames + if(empty($text)) $text = str_replace('_', '\_', $file->name()); + + return html::a($file->url(), html($text), array( + 'class' => $tag->attr('class'), + 'title' => html($tag->attr('title')), + 'rel' => $tag->attr('rel'), + 'target' => $tag->target(), + )); + + } +); + +// image tag +kirbytext::$tags['image'] = array( + 'attr' => array( + 'width', + 'height', + 'alt', + 'text', + 'title', + 'class', + 'imgclass', + 'linkclass', + 'caption', + 'link', + 'target', + 'popup', + 'rel' + ), + 'html' => function($tag) { + + $url = $tag->attr('image'); + $alt = $tag->attr('alt'); + $title = $tag->attr('title'); + $link = $tag->attr('link'); + $caption = $tag->attr('caption'); + $file = $tag->file($url); + + // use the file url if available and otherwise the given url + $url = $file ? $file->url() : url($url); + + // alt is just an alternative for text + if($text = $tag->attr('text')) $alt = $text; + + // try to get the title from the image object and use it as alt text + if($file) { + + if(empty($alt) and $file->alt() != '') { + $alt = $file->alt(); + } + + if(empty($title) and $file->title() != '') { + $title = $file->title(); + } + + } + + // at least some accessibility for the image + if(empty($alt)) $alt = ' '; + + // link builder + $_link = function($image) use($tag, $url, $link, $file) { + + if(empty($link)) return $image; + + // build the href for the link + if($link == 'self') { + $href = $url; + } else if($file and $link == $file->filename()) { + $href = $file->url(); + } else if($tag->file($link)) { + $href = $tag->file($link)->url(); + } else { + $href = $link; + } + + return html::a(url($href), $image, array( + 'rel' => $tag->attr('rel'), + 'class' => $tag->attr('linkclass'), + 'title' => $tag->attr('title'), + 'target' => $tag->target() + )); + + }; + + // image builder + $_image = function($class) use($tag, $url, $alt, $title) { + return html::img($url, array( + 'width' => $tag->attr('width'), + 'height' => $tag->attr('height'), + 'class' => $class, + 'title' => $title, + 'alt' => $alt + )); + }; + + if(kirby()->option('kirbytext.image.figure') or !empty($caption)) { + $image = $_link($_image($tag->attr('imgclass'))); + $figure = new Brick('figure'); + $figure->addClass($tag->attr('class')); + $figure->append($image); + if(!empty($caption)) { + $figure->append('
' . html($caption) . '
'); + } + return $figure; + } else { + $class = trim($tag->attr('class') . ' ' . $tag->attr('imgclass')); + return $_link($_image($class)); + } + + } +); + +// link tag +kirbytext::$tags['link'] = array( + 'attr' => array( + 'text', + 'class', + 'title', + 'rel', + 'lang', + 'target', + 'popup' + ), + 'html' => function($tag) { + + $link = url($tag->attr('link'), $tag->attr('lang')); + $text = $tag->attr('text'); + + if(empty($text)) { + $text = $link; + } + + if(str::isURL($text)) { + $text = url::short($text); + } + + return html::a($link, $text, array( + 'rel' => $tag->attr('rel'), + 'class' => $tag->attr('class'), + 'title' => $tag->attr('title'), + 'target' => $tag->target(), + )); + + } +); + +// tel tag +kirbytext::$tags['tel'] = array( + 'attr' => array( + 'text', + 'class', + 'title' + ), + 'html' => function($tag) { + + $text = $tag->attr('text'); + $tel = str_replace(array('/', ' ', '-'), '', $tag->attr('tel')); + + if(empty($text)) $text = $tag->attr('tel'); + + return html::a('tel:' . $tel, html($text), array( + 'rel' => $tag->attr('rel'), + 'class' => $tag->attr('class'), + 'title' => html($tag->attr('title')) + )); + } +); + + +// twitter tag +kirbytext::$tags['twitter'] = array( + 'attr' => array( + 'class', + 'title', + 'text', + 'rel', + 'target', + 'popup', + ), + 'html' => function($tag) { + + // get and sanitize the username + $username = str_replace('@', '', $tag->attr('twitter')); + + // build the profile url + $url = 'https://twitter.com/' . $username; + + // sanitize the link text + $text = $tag->attr('text', '@' . $username); + + // build the final link + return html::a($url, $text, array( + 'class' => $tag->attr('class'), + 'title' => $tag->attr('title'), + 'rel' => $tag->attr('rel'), + 'target' => $tag->target(), + )); + + } +); + +kirbytext::$tags['youtube'] = array( + 'attr' => array( + 'width', + 'height', + 'class', + 'caption' + ), + 'html' => function($tag) { + + $caption = $tag->attr('caption'); + + if(!empty($caption)) { + $figcaption = '
' . escape::html($caption) . '
'; + } else { + $figcaption = null; + } + + return '
' . embed::youtube($tag->attr('youtube'), array( + 'width' => $tag->attr('width', kirby()->option('kirbytext.video.width')), + 'height' => $tag->attr('height', kirby()->option('kirbytext.video.height')), + 'options' => kirby()->option('kirbytext.video.youtube.options') + )) . $figcaption . '
'; + + } +); + +kirbytext::$tags['vimeo'] = array( + 'attr' => array( + 'width', + 'height', + 'class', + 'caption' + ), + 'html' => function($tag) { + + $caption = $tag->attr('caption'); + + if(!empty($caption)) { + $figcaption = '
' . escape::html($caption) . '
'; + } else { + $figcaption = null; + } + + return '
' . embed::vimeo($tag->attr('vimeo'), array( + 'width' => $tag->attr('width', kirby()->option('kirbytext.video.width')), + 'height' => $tag->attr('height', kirby()->option('kirbytext.video.height')), + 'options' => kirby()->option('kirbytext.video.vimeo.options') + )) . $figcaption . '
'; + + } +); + +kirbytext::$tags['gist'] = array( + 'attr' => array( + 'file' + ), + 'html' => function($tag) { + return embed::gist($tag->attr('gist'), $tag->attr('file')); + } +); diff --git a/kirby/helpers.php b/kirby/helpers.php new file mode 100644 index 0000000..229d6be --- /dev/null +++ b/kirby/helpers.php @@ -0,0 +1,327 @@ +component('snippet')->render($file, $data, $return); +} + +/** + * Builds a css link tag for relative or absolute urls + * + * @param string $url + * @param string $media + * @return string + */ +function css() { + return call([kirby::instance()->component('css'), 'tag'], func_get_args()); +} + +/** + * Builds a script tag for relative or absolute links + * + * @param string $src + * @param boolean $async + * @return string + */ +function js($src, $async = false) { + return call([kirby::instance()->component('js'), 'tag'], func_get_args()); +} + +/** + * Global markdown parser shortcut + * + * @param string $text + * @return string + */ +function markdown($text) { + return kirby::instance()->component('markdown')->parse($text); +} + +/** + * Global smartypants parser shortcut + * + * @param string $text + * @return string + */ +function smartypants($text) { + return kirby::instance()->component('smartypants')->parse($text); +} + +/** + * Converts a string to Kirbytext + * + * @param Field $field + * @return string + */ +function kirbytext($field) { + return (string)new Kirbytext($field); +} + +/** + * Returns the Kirby class singleton + * + * @return Kirby + */ +function kirby($class = null) { + return kirby::instance($class); +} + +/** + * Returns the site object + * + * @return Site + */ +function site() { + return kirby::instance()->site(); +} + +/** + * Returns either the current page or any page for a given uri + * + * @return Page + */ +function page() { + return call_user_func_array(array(kirby::instance()->site(), 'page'), func_get_args()); +} + +/** + * Helper to build page collections + * + * @param array $data + */ +function pages($data = array()) { + return new Pages($data); +} + +/** + * Creates an excerpt without html and kirbytext + * + * @param mixed $text Variable object or string + * @param int $length The number of characters which should be included in the excerpt + * @param array $params an array of options for kirbytext: array('markdown' => true, 'smartypants' => true) + * @return string The shortened text + */ +function excerpt($text, $length = 140, $mode = 'chars') { + + if(strtolower($mode) == 'words') { + $text = str::excerpt(kirbytext($text), 0); + + if(str_word_count($text, 0) > $length) { + $words = str_word_count($text, 2); + $pos = array_keys($words); + $text = str::substr($text, 0, $pos[$length]) . '...'; + } + return $text; + + } else { + return str::excerpt(kirbytext($text), $length); + } + +} + +/** + * Helper to create correct text file names for content files + * + * @param string $uri + * @param string $template + * @param string $lang + * @return string + */ +function textfile($uri, $template, $lang = null) { + + $curi = ''; + $parts = str::split($uri, '/'); + $parent = site(); + + foreach($parts as $p) { + + if($parent and $child = $parent->children()->find($p)) { + $curi .= '/' . $child->dirname(); + $parent = $child; + } else { + $curi .= '/' . $p; + $parent = null; + } + + } + + $uri = ltrim($curi, '/'); + $root = kirby::instance()->roots()->content(); + $ext = kirby::instance()->option('content.file.extension', 'txt'); + return $root . DS . r(!empty($uri), str_replace('/', DS, $uri) . DS) . $template . r($lang, '.' . $lang) . '.' . $ext; + +} + +/** + * Renders a kirbytag + * + * @param array $attr + * @return Kirbytag + */ +function kirbytag($attr) { + return new Kirbytag(null, key($attr), $attr); +} + +/** + * Builds a Youtube video iframe + * + * @param string $url + * @param mixed $width + * @param mixed $height + * @param string $class + * @return string + */ +function youtube($url, $width = null, $height = null, $class = null) { + return kirbytag(array( + 'youtube' => $url, + 'width' => $width, + 'height' => $height, + 'class' => $class + )); +} + +/** + * Builds a Vimeo video iframe + * + * @param string $url + * @param mixed $width + * @param mixed $height + * @param string $class + * @return string + */ +function vimeo($url, $width = null, $height = null, $class = null) { + return kirbytag(array( + 'vimeo' => $url, + 'width' => $width, + 'height' => $height, + 'class' => $class + )); +} + +/** + * Builds a Twitter link + * + * @param string $username + * @param string $text + * @param string $title + * @param string $class + * @return string + */ +function twitter($username, $text = null, $title = null, $class = null) { + return kirbytag(array( + 'twitter' => $username, + 'text' => $text, + 'title' => $title, + 'class' => $class + )); +} + +/** + * Embeds a Github Gist + * + * @param string $url + * @param string $file + * @return string + */ +function gist($url, $file = null) { + return kirbytag(array( + 'gist' => $url, + 'file' => $file, + )); +} + +/** + * Returns the current url + * + * @return string + */ +function thisUrl() { + return url::current(); +} + +/** + * Give this any kind of array + * to get some kirby style structure + * + * @param mixed $data + * @param mixed $page + * @param mixed $key + * @return mixed + */ +function structure($data, $page = null, $key = null) { + + if(is_null($page)) { + $page = page(); + } + + if(is_array($data)) { + $result = new Structure(); + $result->page = $page; + foreach($data as $key => $value) { + $result->append($key, structure($value, $page, $key)); + } + return $result; + } else if(is_a($data, 'Field')) { + return $data; + } else { + return new Field($page, $key, $data); + } + +}; + + +/** + * Return an image from any page + * specified by the path + * + * Example: + * + * + * @param string $path + * @return File|null + */ +function image($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; + } + +} + +/** + * Shortcut to create a new thumb object + * + * @param mixed Either a file path or a Media object + * @param array An array of additional params for the thumb + * @return object Thumb + */ +function thumb($image, $params = array(), $obj = true) { + if(is_a($image, 'File') || is_a($image, 'Asset')) { + return $obj ? $image->thumb($params) : $image->thumb($params)->url(); + } else { + $class = new Thumb($image, $params); + return $obj ? $class : $class->url(); + } +} \ No newline at end of file diff --git a/kirby/kirby.php b/kirby/kirby.php new file mode 100644 index 0000000..0e5b0cc --- /dev/null +++ b/kirby/kirby.php @@ -0,0 +1,785 @@ +roots = new Roots(dirname(__DIR__)); + $this->urls = new Urls(); + $this->registry = new Registry($this); + $this->options = array_merge($this->defaults(), $options); + $this->path = implode('/', (array)url::fragments(detect::path())); + + // make sure the instance is stored / overwritten + static::$instance = $this; + } + + public function defaults() { + + $defaults = array( + 'url' => false, + 'timezone' => 'UTC', + 'license' => null, + 'rewrite' => true, + 'error' => 'error', + 'home' => 'home', + 'locale' => 'en_US.UTF8', + 'routes' => array(), + 'headers' => array(), + 'languages' => array(), + 'roles' => array(), + 'cache' => false, + 'debug' => 'env', + 'ssl' => false, + 'cache.driver' => 'file', + 'cache.options' => array(), + 'cache.ignore' => array(), + 'cache.autoupdate' => true, + 'date.handler' => 'date', + 'kirbytext.video.class' => 'video', + 'kirbytext.video.width' => false, + 'kirbytext.video.height' => false, + 'kirbytext.video.youtube.options' => array(), + 'kirbytext.video.vimeo.options' => array(), + 'kirbytext.image.figure' => true, + 'content.file.extension' => 'txt', + 'content.file.ignore' => array(), + 'content.file.normalize' => false, + 'email.service' => 'mail', + 'email.to' => null, + 'email.replyTo' => null, + 'email.subject' => null, + 'email.body' => null, + 'email.options' => array(), + ); + + return $defaults; + + } + + public function roots() { + return $this->roots; + } + + public function urls() { + return $this->urls; + } + + public function registry() { + return $this->registry; + } + + public function url() { + return $this->urls->index(); + } + + public function options() { + return $this->options; + } + + public function option($key, $default = null) { + return a::get($this->options, $key, $default); + } + + public function path() { + return $this->path; + } + + public function page() { + return $this->page; + } + + public function response() { + return $this->response; + } + + /** + * Install a new entry in the registry + */ + public function set() { + return call_user_func_array([$this->registry, 'set'], func_get_args()); + } + + /** + * Retrieve an entry from the registry + */ + public function get() { + return call_user_func_array([$this->registry, 'get'], func_get_args()); + } + + public function configure() { + + // load all available config files + $root = $this->roots()->config(); + $configs = array( + 'main' => 'config.php', + 'host' => 'config.' . server::get('SERVER_NAME') . '.php', + 'addr' => 'config.' . server::get('SERVER_ADDR') . '.php', + ); + + $allowed = array_filter(dir::read($root), function($file) { + return substr($file, 0, 7) === 'config.' and substr($file, -4) === '.php'; + }); + + foreach($configs as $config) { + $file = $root . DS . $config; + if(in_array($config, $allowed, true) and file_exists($file)) include_once($file); + } + + // apply the options + $this->options = array_merge($this->options, c::$data); + + // overwrite the autodetected url + if($this->options['url']) { + $this->urls->index = $this->options['url']; + } + + // connect the url class with its handlers + url::$home = $this->urls()->index(); + url::$to = $this->option('url.to', function($url = '', $lang = null) { + + if(url::isAbsolute($url)) return $url; + + $start = substr($url, 0, 1); + switch($start) { + case '#': + return $url; + break; + case '.': + return page()->url() . '/' . $url; + break; + default: + if($page = page($url)) { + // use the "official" page url + return $page->url($lang); + } else { + // don't convert absolute urls + return url::makeAbsolute($url); + } + break; + } + + }); + + // setup the pagination redirect to the error page + pagination::$defaults['redirect'] = $this->option('error'); + + // setting up the email class + email::$defaults['service'] = $this->option('email.service'); + email::$defaults['from'] = $this->option('email.from'); + email::$defaults['to'] = $this->option('email.to'); + email::$defaults['replyTo'] = $this->option('email.replyTo'); + email::$defaults['subject'] = $this->option('email.subject'); + email::$defaults['body'] = $this->option('email.body'); + email::$defaults['options'] = $this->option('email.options'); + + // simple error handling + if($this->options['debug'] === true) { + error_reporting(E_ALL); + ini_set('display_errors', 1); + } else if($this->options['debug'] === false) { + error_reporting(0); + ini_set('display_errors', 0); + } + + } + + /** + * Registers all routes + * + * @param array $routes New routes + * @return array + */ + public function routes($routes = array()) { + + // extend the existing routes + if(!empty($routes) and is_array($routes)) { + return $this->options['routes'] = array_merge($this->options['routes'], $routes); + } + + $routes = $this->options['routes']; + $kirby = $this; + $site = $this->site(); + + if($site->multilang()) { + + foreach($site->languages() as $lang) { + + $routes[] = array( + 'pattern' => ltrim($lang->url . '/(:all?)', '/'), + 'method' => 'ALL', + 'lang' => $lang, + 'action' => function($path = null) use($kirby, $site) { + return $site->visit($path, $kirby->route->lang->code()); + } + ); + + } + + // fallback for the homepage + $routes[] = array( + 'pattern' => '/', + 'method' => 'ALL', + 'action' => function() use($kirby, $site) { + + // check if the language detector is activated + if($kirby->option('language.detect')) { + + if(s::get('language') and $language = $kirby->site()->sessionLanguage()) { + // $language is already set but the user wants to + // select the default language + $referer = r::referer(); + if(!empty($referer) && str::startsWith($referer, $this->urls()->index())) { + $language = $kirby->site()->defaultLanguage(); + } + } else { + // detect the user language + $language = $kirby->site()->detectedLanguage(); + } + + } else { + // always use the default language if the detector is disabled + $language = $kirby->site()->defaultLanguage(); + } + + // redirect to the language homepage if necessary + if($language->url != '/' and $language->url != '') { + go($language->url()); + } + + // plain home pages + return $site->visit('/', $language->code()); + + } + ); + + } + + // tinyurl handling + $routes['tinyurl'] = $this->component('tinyurl')->route(); + + // home redirect + $routes['homeRedirect'] = array( + 'pattern' => $this->options['home'], + 'action' => function() { + redirect::send(page('home')->url(), 307); + } + ); + + // plugin assets + $routes['pluginAssets'] = array( + 'pattern' => 'assets/plugins/(:any)/(:all)', + 'method' => 'GET', + 'action' => function($plugin, $path) use($kirby) { + $root = $kirby->roots()->plugins() . DS . $plugin . DS . 'assets' . DS . $path; + $file = new Media($root); + + if($file->exists()) { + return new Response(f::read($root), f::extension($root)); + } else { + return new Response('The file could not be found', f::extension($path), 404); + } + + + } + ); + + // all other urls + $routes['others'] = array( + 'pattern' => '(:all)', + 'method' => 'ALL', + 'action' => function($path = null) use($site, $kirby) { + + // visit the currently active page + $page = $site->visit($path); + + // react on errors for invalid URLs + if($page->isErrorPage() and $page->uri() != $path) { + + // get the filename + $filename = rawurldecode(basename($path)); + $pagepath = dirname($path); + + // check if there's a page for the parent path + if($page = $site->find($pagepath)) { + // check if there's a file for the last element of the path + if($file = $page->file($filename)) { + go($file->url()); + } + } + + // return the error page if there's no such page + return $site->errorPage(); + + } + + return $page; + + } + + ); + + return $routes; + + } + + /** + * Loads all available plugins for the site + * + * @return array + */ + public function plugins() { + + // check for a cached plugins array + if(!is_null($this->plugins)) return $this->plugins; + + // get the plugins root + $root = $this->roots->plugins(); + + // start the plugin registry + $this->plugins = array(); + + // check for an existing plugins dir + if(!is_dir($root)) return $this->plugins; + + foreach(array_diff(scandir($root), array('.', '..')) as $file) { + if(is_dir($root . DS . $file)) { + $this->plugin($file, 'dir'); + } else if(f::extension($file) == 'php') { + $this->plugin(f::name($file), 'file'); + } + } + + return $this->plugins; + + } + + /** + * Loads a single plugin + * + * @param string $name + * @param string $mode + * @return mixed + */ + public function plugin($name, $mode = 'dir') { + + if(isset($this->plugins[$name])) return $this->plugins[$name]; + + if($mode == 'dir') { + $file = $this->roots->plugins() . DS . $name . DS . $name . '.php'; + } else { + $file = $this->roots->plugins() . DS . $name . '.php'; + } + + // make the kirby variable available in plugin files + $kirby = $this; + + if(file_exists($file)) return $this->plugins[$name] = include_once($file); + + return false; + + } + + /** + * Load all default extensions + */ + public function extensions() { + + // load all kirby tags and field methods + include_once(__DIR__ . DS . 'extensions' . DS . 'tags.php'); + include_once(__DIR__ . DS . 'extensions' . DS . 'methods.php'); + + // install additional kirby tags + kirbytext::install($this->roots->tags()); + + } + + /** + * Autoloads all page models + */ + public function models() { + + if(!is_dir($this->roots()->models())) return false; + + $root = $this->roots()->models(); + $files = dir::read($root); + $load = array(); + + foreach($files as $file) { + if(f::extension($file) != 'php') continue; + $name = f::name($file); + $classname = str_replace(array('.', '-', '_'), '', $name . 'page'); + $load[$classname] = $root . DS . $file; + + // register the model + page::$models[$name] = $classname; + } + + // start the autoloader + if(!empty($load)) { + load($load); + } + + } + + public function localize() { + + $site = $this->site(); + + if($site->multilang() and !$site->language()) { + $site->language = $site->languages()->findDefault(); + } + + // set the local for the specific language + if(is_array($site->locale())) { + foreach($site->locale() as $key => $value) { + setlocale($key, $value); + } + } else { + setlocale(LC_ALL, $site->locale()); + } + + // additional language variables for multilang sites + if($site->multilang()) { + // path for the language file + $file = $this->roots()->languages() . DS . $site->language()->code() . '.php'; + // load the file if it exists + if(file_exists($file)) include_once($file); + } + + } + + /** + * Returns the branch file + * + * @return string + */ + public function branch() { + + // which branch? + $branch = count($this->options['languages']) > 0 ? 'multilang' : 'default'; + + // build the path for the branch file + return __DIR__ . DS . 'branches' . DS . $branch . '.php'; + + } + + /** + * Initializes and returns the site object + * depending on the appropriate branch + * + * @return Site + */ + public function site() { + + // check for a cached version of the site object + if(!is_null($this->site)) return $this->site; + + // load all options + $this->configure(); + + // setup the cache + $this->cache(); + + // load the main branch file + include_once($this->branch()); + + // create the site object + return $this->site = new Site($this); + + } + + /** + * Cache setup + * + * @return Cache + */ + public function cache() { + + if(!is_null($this->cache)) return $this->cache; + + // cache setup + if($this->options['cache']) { + if($this->options['cache.driver'] == 'file' and empty($this->options['cache.options'])) { + $this->options['cache.options'] = array( + 'root' => $this->roots()->cache() + ); + } + return $this->cache = cache::setup($this->options['cache.driver'], $this->options['cache.options']); + } else { + return $this->cache = cache::setup('mock'); + } + + } + + /** + * Renders the HTML for the page or fetches it from the cache + * + * @param Page $page + * @param boolean $headers + * @return string + */ + public function render(Page $page, $data = array(), $headers = true) { + + // register the currently rendered page + $this->page = $page; + + // send all headers for the page + if($headers) $page->headers(); + + // configure pagination urls + $query = (string)$this->request()->query(); + $params = (string)$this->request()->params() . r($query, '?') . $query; + + pagination::$defaults['url'] = $page->url() . r($params, '/') . $params; + + // cache the result if possible + if($this->options['cache'] and $page->isCachable()) { + + // try to read the cache by cid (cache id) + $cacheId = md5(url::current()); + + // check for modified content within the content folder + // and auto-expire the page cache in such a case + if($this->options['cache.autoupdate'] and $this->cache()->exists($cacheId)) { + + // get the creation date of the cache file + $created = $this->cache()->created($cacheId); + + // make sure to kill the cache if the site has been modified + if($this->site->wasModifiedAfter($created)) { + $this->cache()->remove($cacheId); + } + + } + + // try to fetch the template from cache + $template = $this->cache()->get($cacheId); + + // fetch fresh content if the cache is empty + if(empty($template)) { + $template = $this->template($page, $data); + // store the result for the next round + $this->cache()->set($cacheId, $template); + } + + return $template; + + } + + // return a fresh template + return $this->template($page, $data); + + } + + /** + * Template configuration + * + * @param Page $page + * @param array $data + * @return string + */ + public function template(Page $page, $data = array()) { + return $this->component('template')->render($page, $data); + } + + public function request() { + if(!is_null($this->request)) return $this->request; + return $this->request = new Request($this); + } + + public function router() { + return $this->router; + } + + public function route() { + return $this->route; + } + + /** + * Starts the router, renders the page and returns the response + * + * @return mixed + */ + public function launch() { + + // this will trigger the configuration + $site = $this->site(); + + // force secure connections if enabled + if($this->option('ssl') and !r::secure()) { + // rebuild the current url with https + go(url::build(array('scheme' => 'https'))); + } + + // set the timezone for all date functions + date_default_timezone_set($this->options['timezone']); + + // load all extensions + $this->extensions(); + + // load all plugins + $this->plugins(); + + // load all models + $this->models(); + + // start the router + $this->router = new Router($this->routes()); + $this->route = $this->router->run($this->path()); + + // check for a valid route + if(is_null($this->route)) { + header::status('500'); + header::type('json'); + die(json_encode(array( + 'status' => 'error', + 'message' => 'Invalid route or request method' + ))); + } + + // call the router action with all arguments from the pattern + $response = call($this->route->action(), $this->route->arguments()); + + // load all language variables + // this can only be loaded once the router action has been called + // otherwise the current language is not yet available + $this->localize(); + + // build the response + $this->response = $this->component('response')->make($response); + + // store the current language in the session + if($this->site()->multilang() && $language = $this->site()->language()) { + s::set('language', $language->code()); + } + + return $this->response; + + } + + /** + * Register a new hook + * + * @param string $hook The name of the hook + * @param closure $callback + */ + public function hook($hook, $callback) { + + if(isset(static::$hooks[$hook]) and is_array(static::$hooks[$hook])) { + static::$hooks[$hook][] = $callback; + } else { + static::$hooks[$hook] = array($callback); + } + + } + + /** + * Trigger a hook + * + * @param string $hook The name of the hook + * @param mixed $args Additional arguments for the hook + * @return mixed + */ + public function trigger($hook, $args = null) { + + if(isset(static::$hooks[$hook]) and is_array(static::$hooks[$hook])) { + foreach(static::$hooks[$hook] as $key => $callback) { + + if(array_key_exists($hook, static::$triggered) && in_array($key, static::$triggered[$hook])) continue; + + static::$triggered[$hook] = $key; + + try { + call($callback, $args); + } catch(Exception $e) { + // caught callback error + } + } + } + } + + static public function start() { + return kirby()->launch(); + } + + /** + * Register and fetch core components + */ + public function component($name, $component = null) { + if(is_null($component)) { + if(!isset($this->components[$name])) { + // load the default component if it exists + if(file_exists(__DIR__ . DS . 'kirby' . DS . 'component' . DS . strtolower($name) . '.php')) { + $this->component($name, 'Kirby\\Component\\' . $name); + } else { + throw new Exception('The component "' . $name . '" does not exist'); + } + } + return $this->components[$name]; + } else { + + if(!is_string($component)) { + throw new Exception('Please provide a valid component name'); + } + + // init the component + $object = new $component($this); + + if(!is_a($object, 'Kirby\\Component')) { + throw new Exception('The component "' . $name . '" must be an instance of the Kirby\\Component class'); + } + + if(!is_a($object, 'Kirby\\Component\\' . $name)) { + throw new Exception('The component "' . $name . '" must be an instance of the Kirby\\Component\\' . ucfirst($name) . ' class'); + } + + // add the component defaults + $this->options = array_merge($object->defaults(), $this->options); + + // configure the component + $object->configure(); + + // register the component + $this->components[$name] = $object; + + } + } + +} \ No newline at end of file diff --git a/kirby/kirby/component.php b/kirby/kirby/component.php new file mode 100644 index 0000000..6ffe1f0 --- /dev/null +++ b/kirby/kirby/component.php @@ -0,0 +1,27 @@ +kirby = $kirby; + } + + public function defaults() { + return []; + } + + public function configure() { + + } + + public function kirby() { + return $this->kirby; + } + +} \ No newline at end of file diff --git a/kirby/kirby/component/css.php b/kirby/kirby/component/css.php new file mode 100644 index 0000000..fc811b3 --- /dev/null +++ b/kirby/kirby/component/css.php @@ -0,0 +1,52 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class CSS extends \Kirby\Component { + + /** + * Builds the html link tag for the given css file + * + * @param string $url + * @param null|string $media + * @return string + */ + public function tag($url, $media = null) { + + if(is_array($url)) { + $css = array(); + foreach($url as $u) $css[] = $this->tag($u, $media); + return implode(PHP_EOL, $css) . PHP_EOL; + } + + // auto template css files + if($url == '@auto') { + + $file = $this->kirby->site()->page()->template() . '.css'; + $root = $this->kirby->roots()->autocss() . DS . $file; + $url = $this->kirby->urls()->autocss() . '/' . $file; + + if(!file_exists($root)) return false; + + } + + return html::tag('link', null, array( + 'rel' => 'stylesheet', + 'href' => url($url), + 'media' => $media + )); + + } + +} \ No newline at end of file diff --git a/kirby/kirby/component/js.php b/kirby/kirby/component/js.php new file mode 100644 index 0000000..6322c9f --- /dev/null +++ b/kirby/kirby/component/js.php @@ -0,0 +1,51 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class JS extends \Kirby\Component { + + /** + * Builds the html script tag for the given javascript file + * + * @param string $src + * @param boolean async + * @return string + */ + public function tag($src, $async = false) { + + if(is_array($src)) { + $js = array(); + foreach($src as $s) $js[] = $this->tag($s, $async); + return implode(PHP_EOL, $js) . PHP_EOL; + } + + // auto template css files + if($src == '@auto') { + + $file = $this->kirby->site()->page()->template() . '.js'; + $root = $this->kirby->roots()->autojs() . DS . $file; + $src = $this->kirby->urls()->autojs() . '/' . $file; + + if(!file_exists($root)) return false; + + } + + return html::tag('script', '', array( + 'src' => url($src), + 'async' => $async + )); + + } + +} \ No newline at end of file diff --git a/kirby/kirby/component/markdown.php b/kirby/kirby/component/markdown.php new file mode 100644 index 0000000..8314958 --- /dev/null +++ b/kirby/kirby/component/markdown.php @@ -0,0 +1,56 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Markdown extends \Kirby\Component { + + /** + * Returns the default options for the component + * + * @return array + */ + public function defaults() { + return [ + 'markdown' => true, + 'markdown.extra' => false, + 'markdown.breaks' => true, + ]; + } + + /** + * Initializes the Parsedown parser and + * transforms the given markdown to HTML + * + * @param string $markdown + * @return string + */ + public function parse($markdown) { + + if(!$this->kirby->options['markdown']) { + return $markdown; + } else { + // initialize the right markdown class + $parsedown = $this->kirby->options['markdown.extra'] ? new ParsedownExtra() : new Parsedown(); + + // set markdown auto-breaks + $parsedown->setBreaksEnabled($this->kirby->options['markdown.breaks']); + + // parse it! + return $parsedown->text($markdown); + } + + } + +} \ No newline at end of file diff --git a/kirby/kirby/component/response.php b/kirby/kirby/component/response.php new file mode 100644 index 0000000..5a90927 --- /dev/null +++ b/kirby/kirby/component/response.php @@ -0,0 +1,38 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Response extends \Kirby\Component { + + /** + * Builds and return the response by various input + * + * @param mixed $response + * @return mixed + */ + public function make($response) { + + if(is_string($response)) { + return $this->kirby->render(page($response)); + } else if(is_array($response)) { + return $this->kirby->render(page($response[0]), $response[1]); + } else if(is_a($response, 'Page')) { + return $this->kirby->render($response); + } else if(is_a($response, 'Response')) { + return $response; + } else { + return null; + } + + } + +} \ No newline at end of file diff --git a/kirby/kirby/component/smartypants.php b/kirby/kirby/component/smartypants.php new file mode 100644 index 0000000..0f74ae4 --- /dev/null +++ b/kirby/kirby/component/smartypants.php @@ -0,0 +1,61 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Smartypants extends \Kirby\Component { + + /** + * Returns the default options for + * the smartypants parser + * + * @return array + */ + public function defaults() { + return [ + 'smartypants' => false, + 'smartypants.attr' => 1, + 'smartypants.doublequote.open' => '“', + 'smartypants.doublequote.close' => '”', + 'smartypants.space.emdash' => ' ', + 'smartypants.space.endash' => ' ', + 'smartypants.space.colon' => ' ', + 'smartypants.space.semicolon' => ' ', + 'smartypants.space.marks' => ' ', + 'smartypants.space.frenchquote' => ' ', + 'smartypants.space.thousand' => ' ', + 'smartypants.space.unit' => ' ', + 'smartypants.skip' => 'pre|code|kbd|script|style|math', + ]; + } + + /** + * Initializes the parser and transforms + * the given text. + * + * @param string $text + * @return string + */ + public function parse($text) { + if(!$this->kirby->options['smartypants']) { + return $text; + } else { + // prepare the text + $text = str_replace('"', '"', $text); + // run the parser + $parser = new SmartyPantsTypographer_Parser($this->kirby->options['smartypants.attr']); + return $parser->transform($text); + } + } + +} \ No newline at end of file diff --git a/kirby/kirby/component/snippet.php b/kirby/kirby/component/snippet.php new file mode 100644 index 0000000..57356d9 --- /dev/null +++ b/kirby/kirby/component/snippet.php @@ -0,0 +1,41 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Snippet extends \Kirby\Component { + + /** + * Returns a snippet file path by name + * + * @param string $name + * @return string + */ + public function file($name) { + return $this->kirby->roots()->snippets() . DS . str_replace('/', DS, $name) . '.php'; + } + + /** + * Renders the snippet with the given data + * + * @param string $name + * @param array $data + * @param boolean $return + * @return string + */ + public function render($name, $data = [], $return = false) { + if(is_object($data)) $data = ['item' => $data]; + return tpl::load($this->kirby->registry->get('snippet', $name), $data, $return); + } + +} \ No newline at end of file diff --git a/kirby/kirby/component/template.php b/kirby/kirby/component/template.php new file mode 100644 index 0000000..21f5ffd --- /dev/null +++ b/kirby/kirby/component/template.php @@ -0,0 +1,89 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Template extends \Kirby\Component { + + /** + * Collects all template data by page + * + * @param mixed $page + * @param array $data + * @return array + */ + public function data($page, $data = []) { + + if($page instanceof Page) { + $data = array_merge( + $page->templateData(), + $data, + $page->controller($data) + ); + } + + // apply the basic template vars + return array_merge(array( + 'kirby' => $this->kirby, + 'site' => $this->kirby->site(), + 'pages' => $this->kirby->site()->children(), + 'page' => $page + ), $data); + + } + + /** + * Returns a template file path by name + * + * @param string $name + * @return string + */ + public function file($name) { + return $this->kirby->roots()->templates() . DS . str_replace('/', DS, $name) . '.php'; + } + + /** + * Renders the template by page with the additional data + * + * @param Page|string $template + * @param array $data + * @param boolean $return + * @return string + */ + public function render($template, $data = [], $return = true) { + + if($template instanceof Page) { + $page = $template; + $file = $page->templateFile(); + $data = $this->data($page, $data); + } else { + $file = $template; + $data = $this->data(null, $data); + } + + // check for an existing template + if(!file_exists($file)) { + throw new Exception('The template could not be found'); + } + + // merge and register the template data globally + tpl::$data = array_merge(tpl::$data, $data); + + // load the template + return tpl::load($file, null, $return); + + } + +} \ No newline at end of file diff --git a/kirby/kirby/component/thumb.php b/kirby/kirby/component/thumb.php new file mode 100644 index 0000000..8532007 --- /dev/null +++ b/kirby/kirby/component/thumb.php @@ -0,0 +1,206 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Thumb extends Component { + + /** + * Returns the default options for the thumb component + * + * @return array + */ + public function defaults() { + + $self = $this; + + return [ + 'thumbs.driver' => 'gd', + 'thumbs.bin' => 'convert', + 'thumbs.interlace' => false, + 'thumbs.quality' => 90, + 'thumbs.memory' => '128M', + 'thumbs.filename' => false, + 'thumbs.destination' => function($thumb) use($self) { + + $path = $self->path($thumb); + + return new Obj([ + 'root' => $self->kirby->roots()->thumbs() . DS . str_replace('/', DS, $path), + 'url' => $self->kirby->urls()->thumbs() . '/' . $path, + ]); + + } + ]; + } + + /** + * Configures the thumb driver + */ + public function configure() { + + $self = $this; + + // setup the thumbnail location + generator::$defaults['root'] = $this->kirby->roots->thumbs(); + generator::$defaults['url'] = $this->kirby->urls->thumbs(); + + // setup the default thumbnail options + generator::$defaults['driver'] = $this->kirby->option('thumbs.driver'); + generator::$defaults['bin'] = $this->kirby->option('thumbs.bin'); + generator::$defaults['quality'] = $this->kirby->option('thumbs.quality'); + generator::$defaults['interlace'] = $this->kirby->option('thumbs.interlace'); + generator::$defaults['memory'] = $this->kirby->option('thumbs.memory'); + generator::$defaults['destination'] = $this->kirby->option('thumbs.destination'); + generator::$defaults['filename'] = $this->kirby->option('thumbs.filename'); + + } + + public function create($file, $params) { + + if(!$file->isWebsafe()) { + return $file; + } + + $thumb = new Generator($file, $params); + $asset = new Asset($thumb->result); + + // store a reference to the original file + $asset->original($file); + + return $thumb->exists() ? $asset : $file; + + } + + /** + * Returns the clean path for a thumbnail + * + * @param Generator $thumb + * @return string + */ + protected function path(Generator $thumb) { + return ltrim($this->dir($thumb) . '/' . $this->filename($thumb), '/'); + } + + /** + * @param Generator $thumb + * @return string + */ + protected function dir(Generator $thumb) { + if(is_a($thumb->source, 'File')) { + return $thumb->source->page()->id(); + } else { + return str_replace($this->kirby->urls()->index(), '', dirname($thumb->source->url())); + } + } + + /** + * Returns the filename for a thumb including the + * identifying option hash + * + * @param Generator $thumb + * @return string + */ + protected function filename(Generator $thumb) { + + $dimensions = $this->dimensions($thumb); + $wh = $dimensions->width() . 'x' . $dimensions->height(); + $safeName = f::safeName($thumb->source->name()); + $options = $this->options($thumb); + $extension = $thumb->source->extension(); + + if($thumb->options['filename'] === false) { + return $safeName . '-' . $wh . r($options, '-' . $options) . '.' . $extension; + } else { + return str::template($thumb->options['filename'], [ + 'extension' => $extension, + 'name' => $thumb->source->name(), + 'filename' => $thumb->source->filename(), + 'safeName' => $safeName, + 'safeFilename' => $safeName . '.' . $extension, + 'width' => $dimensions->width(), + 'height' => $dimensions->height(), + 'dimensions' => $wh, + 'options' => $options, + 'hash' => md5($thumb->source->root() . $thumb->settingsIdentifier()), + ]); + } + + } + + /** + * Returns an identifying option hash for thumb filenames + * + * @param Generator $thumb + * @return string + */ + protected function options(Generator $thumb) { + + $keys = [ + 'blur' => 'blur', + 'grayscale' => 'bw', + 'quality' => 'q', + ]; + + $string = []; + + foreach($keys as $long => $key) { + + $value = a::get($thumb->options, $long); + + if($value === true) { + $string[] = $key; + } else if($value === false) { + continue; + } else if($key === 'q' && $value == generator::$defaults['quality']) { + // ignore the default quality setting + continue; + } else { + $string[] = $key . $value; + } + + } + + return implode('-', array_filter($string)); + + } + + /** + * @param Generator $thumb + * @return string + */ + protected function dimensions(Generator $thumb) { + + $dimensions = clone $thumb->source->dimensions(); + + if(isset($thumb->options['crop']) && $thumb->options['crop']) { + $dimensions->crop(a::get($thumb->options, 'width'), a::get($thumb->options, 'height')); + } else { + $dimensions->resize(a::get($thumb->options, 'width'), a::get($thumb->options, 'height'), a::get($thumb->options, 'upscale')); + } + + return $dimensions; + + } + +} diff --git a/kirby/kirby/component/tinyurl.php b/kirby/kirby/component/tinyurl.php new file mode 100644 index 0000000..6575c4e --- /dev/null +++ b/kirby/kirby/component/tinyurl.php @@ -0,0 +1,54 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class TinyUrl extends \Kirby\Component { + + /** + * Returns the default options for the tinyurl component + * + * @return array + */ + public function defaults() { + return [ + 'tinyurl.enabled' => true, + 'tinyurl.folder' => 'x', + ]; + } + + /** + * Returns the tinyurl fetching route + * + * @return array + */ + public function route() { + if(!$this->kirby->options['tinyurl.enabled']) { + return false; + } else { + return [ + 'pattern' => $this->kirby->options['tinyurl.folder'] . '/(:any)/(:any?)', + 'action' => function($hash, $lang = null) { + // get the site object + $site = site(); + // make sure the language is set + $site->visit('/', $lang); + // find the page by it's tiny hash + if($page = $site->index()->findBy('hash', $hash)) { + go($page->url($lang)); + } else { + return $site->errorPage(); + } + } + ]; + } + } +} \ No newline at end of file diff --git a/kirby/kirby/registry.php b/kirby/kirby/registry.php new file mode 100644 index 0000000..e420cb1 --- /dev/null +++ b/kirby/kirby/registry.php @@ -0,0 +1,118 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Registry { + + /** + * Kirby Instance + * + * @var Kirby + */ + protected $kirby; + + /** + * @param Kirby $kirby + */ + public function __construct(Kirby $kirby) { + + $this->kirby = $kirby; + + // start the registry entry autoloader + load([ + 'kirby\\registry\\entry' => __DIR__ . DS . 'registry' . DS . 'entry.php', + 'kirby\\registry\\blueprint' => __DIR__ . DS . 'registry' . DS . 'blueprint.php', + 'kirby\\registry\\component' => __DIR__ . DS . 'registry' . DS . 'component.php', + 'kirby\\registry\\controller' => __DIR__ . DS . 'registry' . DS . 'controller.php', + 'kirby\\registry\\hook' => __DIR__ . DS . 'registry' . DS . 'hook.php', + 'kirby\\registry\\field' => __DIR__ . DS . 'registry' . DS . 'field.php', + 'kirby\\registry\\method' => __DIR__ . DS . 'registry' . DS . 'method.php', + 'kirby\\registry\\model' => __DIR__ . DS . 'registry' . DS . 'model.php', + 'kirby\\registry\\option' => __DIR__ . DS . 'registry' . DS . 'option.php', + 'kirby\\registry\\route' => __DIR__ . DS . 'registry' . DS . 'route.php', + 'kirby\\registry\\snippet' => __DIR__ . DS . 'registry' . DS . 'snippet.php', + 'kirby\\registry\\template' => __DIR__ . DS . 'registry' . DS . 'template.php', + 'kirby\\registry\\tag' => __DIR__ . DS . 'registry' . DS . 'tag.php', + 'kirby\\registry\\widget' => __DIR__ . DS . 'registry' . DS . 'widget.php', + ]); + + } + + /** + * Returns the Kirby instance + * + * @return Kirby + */ + public function kirby() { + return $this->kirby; + } + + /** + * Returns a registry entry object by type + * + * @param string $type + * @param string $subtype + * @return Kirby\Registry\Entry + */ + public function entry($type, $subtype = null) { + + $class = 'kirby\\registry\\' . $type; + + if(!class_exists('kirby\\registry\\' . $type)) { + + if(str::contains($type, '::')) { + $parts = str::split($type, '::'); + $subtype = $parts[0]; + $type = $parts[1]; + return $this->entry($type, $subtype); + } + + throw new Exception('Unsupported registry entry type: ' . $type); + + } + + return new $class($this, $subtype); + + } + + /** + * Adds a new entry to the registry + * This will initialize a registry object + * and call the set method of it + * with the passed arguments + */ + public function set() { + $args = func_get_args(); + $type = strtolower(array_shift($args)); + return $this->entry($type)->call('set', $args); + } + + /** + * Retrieves an entry from the registry + * + * This will initialize a registry object + * and call the get method of it + * with the passed arguments + * + * @return Entry + */ + public function get() { + $args = func_get_args(); + $type = array_shift($args); + return $this->entry($type)->call('get', $args); + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/blueprint.php b/kirby/kirby/registry/blueprint.php new file mode 100644 index 0000000..351e69c --- /dev/null +++ b/kirby/kirby/registry/blueprint.php @@ -0,0 +1,69 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Blueprint extends Entry { + + /** + * Blueprint store + * + * @var array $blueprints + */ + protected static $blueprints = []; + + /** + * Adds a new blueprint entry + * + * Pass a path to an existing blueprint file + * to add it to the registry + * + * @param string $name + * @param string $path + * @return $path + */ + public function set($name, $path) { + + if(!$this->kirby->option('debug') || file_exists($path)) { + return static::$blueprints[$name] = $path; + } + + throw new Exception('The blueprint does not exist at the specified path: ' . $path); + + } + + /** + * Retreives a registered blueprint file path + * + * @param string $name + * @return string + */ + public function get($name = null) { + + if(is_null($name)) { + return static::$blueprints; + } + + $file = f::resolve($this->kirby->roots()->blueprints() . DS . str_replace('/', DS, $name), ['php', 'yml', 'yaml']); + + if(file_exists($file)) { + return $file; + } else { + return a::get(static::$blueprints, $name); + } + + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/component.php b/kirby/kirby/registry/component.php new file mode 100644 index 0000000..3679773 --- /dev/null +++ b/kirby/kirby/registry/component.php @@ -0,0 +1,39 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Component extends Entry { + + /** + * Adds a new core component to Kirby + * + * This will directly call the component method of the + * Kirby instance to register the component + * + * @param string $name The name of the component + * @param string $class A valid component classname. Must be extend the according Kirby component type class + */ + public function set($name, $class) { + return $this->kirby->component($name, $class); + } + + /** + * Retreives a component from the Kirby component registry + * + * @param string $name + * @return Kirby\Component + */ + public function get($name) { + return $this->kirby->component($name); + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/controller.php b/kirby/kirby/registry/controller.php new file mode 100644 index 0000000..3fd39a2 --- /dev/null +++ b/kirby/kirby/registry/controller.php @@ -0,0 +1,78 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Controller extends Entry { + + /** + * Store of registered controllers + * + * @var array $controllers + */ + protected static $controllers = []; + + /** + * Adds a new controller to the registry + * + * @param string $name + * @param Closure $callback Must be a valid controller callback + * @return Closure + */ + public function set($name, $callback) { + + $name = strtolower($name); + + if($name === 'site') { + throw new Exception('You are not allowed to set the site controller'); + } + + if(!$this->kirby->option('debug') || is_a($callback, 'Closure') || file_exists($callback)) { + return static::$controllers[$name] = $callback; + } else { + throw new Exception('Invalid controller. You must pass a closure or an existing file'); + } + + } + + /** + * Retreives a controller from the registry + * + * @param string $name + * @return Closure + */ + public function get($name) { + + $name = strtolower($name); + $file = $this->kirby->roots()->controllers() . DS . $name . '.php'; + + if(file_exists($file)) { + return include_once $file; + } + + if(isset(static::$controllers[$name])) { + if(is_a(static::$controllers[$name], 'Closure')) { + return static::$controllers[$name]; + } else if(file_exists(static::$controllers[$name])) { + return include_once static::$controllers[$name]; + } + } + + if(file_exists($this->kirby->roots()->controllers() . DS . 'site.php')) { + return include_once $this->kirby->roots()->controllers() . DS . 'site.php'; + } + + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/entry.php b/kirby/kirby/registry/entry.php new file mode 100644 index 0000000..9d14009 --- /dev/null +++ b/kirby/kirby/registry/entry.php @@ -0,0 +1,98 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Entry { + + /** + * Kirby instance + * + * @var Kirby + */ + protected $kirby; + + /** + * Kirby Registry instance + * + * @var Kirby\Registry + */ + protected $registry; + + /** + * Optional subtype for something + * like $kirby->set('field::method', '…') + * where `field` is the subtype of type `method`. + * + * @param string $subtype + */ + protected $subtype; + + /** + * @param Kirby $kirby + * @param Kirby\Registry $registry + * @param string $subtype + */ + public function __construct(Registry $registry, $subtype = null) { + $this->registry = $registry; + $this->kirby = $registry->kirby(); + $this->subtype = $subtype; + } + + /** + * Interface to call any registry entry method + * + * Mostly used for set() and get() + * + * @param string $method + * @param array $args + * @return mixed + */ + public function call($method, $args) { + return call([$this, $method], $args); + } + + /** + * Returns the Kirby instance + * + * @return Kirby + */ + public function kirby() { + return $this->kirby; + } + + /** + * Returns the Registry instance + * + * @return Kirby\Registry + */ + public function registry() { + return $this->registry; + } + + /** + * Returns the optional subtype + * + * @return string + */ + public function subtype() { + return $this->subtype; + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/field.php b/kirby/kirby/registry/field.php new file mode 100644 index 0000000..1c60f72 --- /dev/null +++ b/kirby/kirby/registry/field.php @@ -0,0 +1,68 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Field extends Entry { + + /** + * Store for registered fields + * + * @var array $fields + */ + protected static $fields = []; + + /** + * Adds a new field to the registry + * + * @param string $name + * @param string $root valid field directory path + * @return Obj generic Kirby object with info about the field + */ + public function set($name, $root) { + + $name = strtolower($name); + $file = $root . DS . $name . '.php'; + + if(!$this->kirby->option('debug') || (is_dir($root) && is_file($file))) { + return static::$fields[$name] = new Obj([ + 'root' => $root, + 'file' => $file, + 'name' => $name, + 'class' => $name . 'field', + ]); + } + + throw new Exception('The field does not exist at the specified path: ' . $root); + + } + + /** + * Retreives a field info object from the registry + * + * @param string|null $name If null, all registered fields will be returned as array + * @param Obj|null|array + */ + public function get($name = null) { + + if(is_null($name)) { + return static::$fields; + } + + return a::get(static::$fields, $name); + + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/hook.php b/kirby/kirby/registry/hook.php new file mode 100644 index 0000000..970ef96 --- /dev/null +++ b/kirby/kirby/registry/hook.php @@ -0,0 +1,30 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Hook extends Entry { + + /** + * Registers a new hook + * + * This will directly call the $kirby->hook() method + * A hook has to be a valid closure + * + * @param string $name + * @param Closure $callback + * @return Closure + */ + public function set($name, $callback) { + return $this->kirby->hook($name, $callback); + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/method.php b/kirby/kirby/registry/method.php new file mode 100644 index 0000000..d91dec3 --- /dev/null +++ b/kirby/kirby/registry/method.php @@ -0,0 +1,71 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Method extends Entry { + + /** + * List of allowed subtypes + * + * @var array $subtypes + */ + protected $subtypes = ['site', 'page', 'pages', 'file', 'files', 'field']; + + /** + * @param Kirby\Registry $registry + * @param string $subtype + */ + public function __construct(Registry $registry, $subtype) { + parent::__construct($registry, $subtype); + if(!in_array($this->subtype, $this->subtypes)) { + throw new Exception('Invalid method type: ' . $this->subtype . '::method'); + } + } + + /** + * Adds a new method to the registry + * + * A method can be registered for any of the allowed + * subtypes, by using the static method syntax: + * $kirby->set('page::method') + * $kirby->set('field::method') + * etc. + * + * The first part of the name is the subtype. + * The second part of the name is the main type (`method` in this case) + * + * @param string $name + * @param Closure $callback + * @return Closure + */ + public function set($name, $callback) { + $class = $this->subtype; + return $class::$methods[$name] = $callback; + } + + /** + * Retrieves a registered method + * + * @param string $name + * @return Closure + */ + public function get($name) { + $class = $this->subtype; + return a::get($class::$methods, $name); + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/model.php b/kirby/kirby/registry/model.php new file mode 100644 index 0000000..09cd72d --- /dev/null +++ b/kirby/kirby/registry/model.php @@ -0,0 +1,77 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Model extends Entry { + + /** + * List of allowed subtypes + * + * @var array $subtypes + */ + protected $subtypes = ['page']; + + /** + * @param Kirby\Registry $registry + * @param string $subtype + */ + public function __construct(Registry $registry, $subtype) { + parent::__construct($registry, $subtype); + if(!in_array($this->subtype, $this->subtypes)) { + throw new Exception('Invalid model type: ' . $this->subtype . '::model'); + } + } + + /** + * Adds a new model to the registry + * + * A model can be registered for any of the allowed + * subtypes, by using the static method syntax: + * + * $kirby->set('page::model') + * + * The first part of the name is the subtype. + * The second part of the name is the main type (`model` in this case) + * + * @param string $name + * @param string $classname Must be a valid classname of a loaded/auto-loaded class + * @return string + */ + public function set($name, $classname) { + + $class = $this->subtype; + + if(!class_exists($classname)) { + throw new Exception('The model class does not exist: ' . $classname); + } + + return $class::$models[$name] = $classname; + + } + + /** + * Retrieves a registered model + * + * @param string $name + * @return string + */ + public function get($name) { + $class = $this->subtype; + return a::get($class::$models, $name); + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/option.php b/kirby/kirby/registry/option.php new file mode 100644 index 0000000..03cd176 --- /dev/null +++ b/kirby/kirby/registry/option.php @@ -0,0 +1,42 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Option extends Entry { + + /** + * Sets a Kirby option + * + * This directly adds passed options to the + * $kirby->options array and is just a convenient + * way to do this through the registry + * + * @param string $key + * @param mixed $value + * @return mixed + */ + public function set($key, $value) { + return $this->kirby->options[$key] = $value; + } + + /** + * Retreives an option from the $kirby->$options array + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function get($key, $default = null) { + return $this->kirby->option($key, $default); + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/route.php b/kirby/kirby/registry/route.php new file mode 100644 index 0000000..e812c2d --- /dev/null +++ b/kirby/kirby/registry/route.php @@ -0,0 +1,28 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Route extends Entry { + + /** + * Registers a new route + * + * This will directly add a route to + * Kirby's route system, by calling $kirby->routes() + * + * @param string $attr + */ + public function set($attr) { + $this->kirby->routes([$attr]); + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/snippet.php b/kirby/kirby/registry/snippet.php new file mode 100644 index 0000000..9158fe5 --- /dev/null +++ b/kirby/kirby/registry/snippet.php @@ -0,0 +1,64 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Snippet extends Entry { + + /** + * List of registered snippet files + * + * @var array $snippets + */ + protected static $snippets = []; + + /** + * Registers a new snippet file + * + * You must pass an existing file in order + * to register it as a valid snippet + * + * @param string $name The name of the snippet. Can contain slashes (i.e. form/field) + * @param string $path + * @return string + */ + public function set($name, $path) { + + if(!$this->kirby->option('debug') || file_exists($path)) { + return static::$snippets[$name] = $path; + } + + throw new Exception('The snippet does not exist at the specified path: ' . $path); + + } + + /** + * Retrieve the file path for a registered snippet + * + * @param string $name + * @return string + */ + public function get($name) { + + $file = $this->kirby->component('snippet')->file($name); + + if(file_exists($file)) { + return $file; + } else { + return a::get(static::$snippets, $name); + } + + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/tag.php b/kirby/kirby/registry/tag.php new file mode 100644 index 0000000..740d84a --- /dev/null +++ b/kirby/kirby/registry/tag.php @@ -0,0 +1,42 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Tag extends Entry { + + /** + * Registers a new kirby tag array + * + * This will directly add the tag to the + * kirbytext::$tags array. + * + * @param string $name + * @param array $tag + */ + public function set($name, $tag) { + kirbytext::$tags[$name] = $tag; + } + + /** + * Retreives a registered kirby tag + * + * @param string $name + * @return array + */ + public function get($name) { + return a::get(kirbytext::$tags, $name); + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/template.php b/kirby/kirby/registry/template.php new file mode 100644 index 0000000..b1d1df0 --- /dev/null +++ b/kirby/kirby/registry/template.php @@ -0,0 +1,62 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Template extends Entry { + + /** + * List of registered template files + * + * @var array $templates + */ + protected static $templates = []; + + /** + * Registers a new template file + * + * Must be an existing file + * + * @param string $name + * @param string $path + */ + public function set($name, $path) { + + if(!$this->kirby->option('debug') || file_exists($path)) { + return static::$templates[$name] = $path; + } + + throw new Exception('The template does not exist at the specified path: ' . $path); + + } + + /** + * Retrieves a registered template file + * + * @param string $name + * @return string + */ + public function get($name) { + + $file = $this->kirby->component('template')->file($name); + + if(file_exists($file)) { + return $file; + } else { + return a::get(static::$templates, $name); + } + + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/widget.php b/kirby/kirby/registry/widget.php new file mode 100644 index 0000000..4d1ff85 --- /dev/null +++ b/kirby/kirby/registry/widget.php @@ -0,0 +1,67 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Widget extends Entry { + + /** + * List of registered widget directories + * + * @var array $widgets + */ + protected static $widgets = []; + + /** + * Registers a new widget + * + * You must pass an existing widget directory + * + * @param string $name + * @param string $path + * @return string + */ + public function set($name, $path) { + + if(!$this->kirby->option('debug') || is_dir($path)) { + return static::$widgets[$name] = $path; + } + + throw new Exception('The widget does not exist at the specified path: ' . $path); + + } + + /** + * Retreives a registered widget directory + * + * @param string|null $name If null, all registered widgets will be returned as array + * @return string|array + */ + public function get($name = null) { + + if(is_null($name)) { + return static::$widgets; + } + + $file = $this->kirby->roots()->widgets() . DS . str_replace('/', DS, $name) . '.php'; + + if(file_exists($file)) { + return $file; + } else { + return a::get(static::$widgets, $name); + } + + } + +} \ No newline at end of file diff --git a/kirby/kirby/request.php b/kirby/kirby/request.php new file mode 100644 index 0000000..5b5b0d5 --- /dev/null +++ b/kirby/kirby/request.php @@ -0,0 +1,41 @@ +kirby = $kirby; + } + + public function url() { + return url::current(); + } + + public function params() { + return new Request\Params(url::params()); + } + + public function query() { + return new Request\Query(url::query()); + } + + public function path() { + return new Request\Path($this->kirby->path()); + } + + public function __call($method, $arguments) { + if(method_exists('r', $method)) { + return call('r::' . $method, $arguments); + } else { + throw new Exception('Invalid method: ' . $method); + } + } + +} diff --git a/kirby/kirby/request/params.php b/kirby/kirby/request/params.php new file mode 100644 index 0000000..ccdb541 --- /dev/null +++ b/kirby/kirby/request/params.php @@ -0,0 +1,22 @@ + $value) { + $params[] = $key . url::paramSeparator() . $value; + } + + return implode('/', $params); + + } + +} \ No newline at end of file diff --git a/kirby/kirby/request/path.php b/kirby/kirby/request/path.php new file mode 100644 index 0000000..dda2af0 --- /dev/null +++ b/kirby/kirby/request/path.php @@ -0,0 +1,18 @@ +data); + } + +} \ No newline at end of file diff --git a/kirby/kirby/request/query.php b/kirby/kirby/request/query.php new file mode 100644 index 0000000..bd58f8b --- /dev/null +++ b/kirby/kirby/request/query.php @@ -0,0 +1,13 @@ +index = $index; + } + + public function content() { + return isset($this->content) ? $this->content : $this->index . DS . 'content'; + } + + public function site() { + return isset($this->site) ? $this->site : $this->index . DS . 'site'; + } + + public function kirby() { + return isset($this->kirby) ? $this->kirby : $this->index . DS . 'kirby'; + } + + public function thumbs() { + return isset($this->thumbs) ? $this->thumbs : $this->index . DS . 'thumbs'; + } + + public function assets() { + return isset($this->assets) ? $this->assets : $this->index . DS . 'assets'; + } + + public function autocss() { + return isset($this->autocss) ? $this->autocss : $this->assets() . DS . 'css' . DS . 'templates'; + } + + public function autojs() { + return isset($this->autojs) ? $this->autojs : $this->assets() . DS . 'js' . DS . 'templates'; + } + + public function avatars() { + return isset($this->avatars) ? $this->avatars : $this->assets() . DS . 'avatars'; + } + + public function config() { + return $this->site() . DS . 'config'; + } + + public function accounts() { + return isset($this->accounts) ? $this->accounts : $this->site() . DS . 'accounts'; + } + + public function blueprints() { + return $this->site() . DS . 'blueprints'; + } + + public function plugins() { + return $this->site() . DS . 'plugins'; + } + + public function cache() { + return isset($this->cache) ? $this->cache : $this->site() . DS . 'cache'; + } + + public function tags() { + return $this->site() . DS . 'tags'; + } + + public function fields() { + return $this->site() . DS . 'fields'; + } + + public function widgets() { + return $this->site() . DS . 'widgets'; + } + + public function controllers() { + return $this->site() . DS . 'controllers'; + } + + public function models() { + return $this->site() . DS . 'models'; + } + + public function templates() { + return $this->site() . DS . 'templates'; + } + + public function snippets() { + return $this->site() . DS . 'snippets'; + } + + public function languages() { + return $this->site() . DS . 'languages'; + } + +} diff --git a/kirby/kirby/traits/image.php b/kirby/kirby/traits/image.php new file mode 100644 index 0000000..5cd43a4 --- /dev/null +++ b/kirby/kirby/traits/image.php @@ -0,0 +1,207 @@ +original; + } else { + $this->original = $original; + return $this; + } + } + + /** + * Creates a thumbnail for the image + * + * @param array $params + * @return Asset + */ + public function thumb($params = []) { + // don't scale thumbs further down + if($this->original()) { + throw new Exception('Thumbnails cannot be modified further'); + } else { + return $this->kirby->component('thumb')->create($this, $params); + } + } + + /** + * Scales the image if possible + * + * @param int $width + * @param mixed $height + * @param mixed $quality + * @return Asset + */ + public function resize($width, $height = null, $quality = null) { + + $params = ['width' => $width]; + + if($height) $params['height'] = $height; + if($quality) $params['quality'] = $quality; + + return $this->thumb($params); + + } + + /** + * Scales and crops the image if possible + * + * @param int $width + * @param mixed $height + * @param mixed $quality + * @return Asset + */ + public function crop($width, $height = null, $quality = null) { + + $params = ['width' => $width, 'crop' => true]; + + if($height) $params['height'] = $height; + if($quality) $params['quality'] = $quality; + + return $this->thumb($params); + + } + + /** + * Scales the width of the image + * + * @param int $width + * @param mixed $quality + * @return Asset + */ + public function width($width = null, $quality = null) { + + if($width === null) { + return parent::width(); + } + + $params = ['width' => $width]; + + if($quality) $params['quality'] = $quality; + + return $this->thumb($params); + + } + + /** + * Scales the height of the image + * + * @param int $height + * @param mixed $quality + * @return Asset + */ + public function height($height = null, $quality = null) { + + if($height === null) { + return parent::height(); + } + + $params = ['height' => $height]; + + if($quality) $params['quality'] = $quality; + + return $this->thumb($params); + + } + + /** + * + */ + public function ratio($ratio = null) { + + if($ratio === null) { + return parent::ratio(); + } + + if($this->isLandscape() || $this->isSquare()) { + $width = $this->width(); + $height = round($width / $ratio); + } else { + $height = $this->height(); + $width = round($height * $ratio); + } + + return $this->crop($width, $height); + + } + + /** + * + */ + public function scale($value) { + return $this->thumb(['width' => $this->width() * $value, 'upscale' => true]); + } + + /** + * Converts the image to grayscale + * + * @return Asset + */ + public function bw() { + return $this->thumb(['grayscale' => true]); + } + + /** + * Blurs the image + * + * @return Asset + */ + public function blur() { + return $this->thumb(['blur' => true]); + } + + /** + * Checks if the asset is a thumbnail + * + * @return boolean + */ + public function isThumb() { + return str::startsWith($this->url(), $this->kirby->urls()->thumbs()); + } + + /** + * Check if the file/image has a websafe format + * + * @return boolean + */ + public function isWebsafe() { + return in_array(strtolower($this->extension()), ['jpg', 'jpeg', 'gif', 'png']); + } + + /** + * Makes it possible to echo the entire object + * + * @return string + */ + public function __toString() { + if($this->isWebsafe()) { + return (string)$this->html(); + } else { + return (string)$this->root; + } + } + +} \ No newline at end of file diff --git a/kirby/kirby/urls.php b/kirby/kirby/urls.php new file mode 100644 index 0000000..1da8565 --- /dev/null +++ b/kirby/kirby/urls.php @@ -0,0 +1,47 @@ +index)) return $this->index; + + if(r::cli()) { + return $this->index = '/'; + } else { + return $this->index = url::base() . preg_replace('!\/index\.php$!i', '', server::get('SCRIPT_NAME')); + } + + } + + public function content() { + return isset($this->content) ? $this->content : url::makeAbsolute('content', $this->index); + } + + public function thumbs() { + return isset($this->thumbs) ? $this->thumbs : url::makeAbsolute('thumbs', $this->index); + } + + public function assets() { + return isset($this->assets) ? $this->assets : url::makeAbsolute('assets', $this->index); + } + + public function autocss() { + return isset($this->autocss) ? $this->autocss : $this->assets() . '/css/templates'; + } + + public function autojs() { + return isset($this->autojs) ? $this->autojs : $this->assets() . '/js/templates'; + } + + public function avatars() { + return isset($this->avatars) ? $this->avatars : $this->assets() . '/avatars'; + } + +} \ No newline at end of file diff --git a/kirby/lib/pageextension.php b/kirby/lib/pageextension.php new file mode 100644 index 0000000..4517394 --- /dev/null +++ b/kirby/lib/pageextension.php @@ -0,0 +1,17 @@ +parent(), $page->dirname()); + } else { + throw new Exception('The page could not be found'); + } + } +} \ No newline at end of file diff --git a/kirby/lib/structure.php b/kirby/lib/structure.php new file mode 100644 index 0000000..4d8ff81 --- /dev/null +++ b/kirby/lib/structure.php @@ -0,0 +1,42 @@ +data[$key])) { + return $this->data[$key]; + } else { + $lowerkeys = array_change_key_case($this->data, CASE_LOWER); + $lowerkey = strtolower($key); + if(isset($lowerkeys[$lowerkey])) { + return $lowerkeys[$lowerkey]; + } + } + + return new Field($this->page, $key, null); + + } + + /** + * Get formatted date fields + * + * @param string $format + * @param string $field + * @return string + */ + public function date($format = null, $field = 'date') { + + if($timestamp = strtotime($this->get($field))) { + if(is_null($format)) { + return $timestamp; + } else { + return kirby()->options['date.handler']($format, $timestamp); + } + } + + } + +} \ No newline at end of file diff --git a/kirby/readme.md b/kirby/readme.md new file mode 100644 index 0000000..78ec751 --- /dev/null +++ b/kirby/readme.md @@ -0,0 +1,18 @@ +# Kirby Core + +This is the Kirby Core submodule. + +Please refer to the [Kirby Starterkit](http://github.com/getkirby/starterkit) +for a complete installation of Kirby + +![Build Status](https://travis-ci.org/getkirby/kirby.svg?branch=master) + +## Author +Bastian Allgeier + + +## Website + + +## License + diff --git a/kirby/system.php b/kirby/system.php new file mode 100644 index 0000000..2338915 --- /dev/null +++ b/kirby/system.php @@ -0,0 +1,21 @@ +roots->index = $root; +$kirby->roots->site = $rootSite; +$kirby->roots->content = $rootContent; + +// render +echo $kirby->launch(); \ No newline at end of file diff --git a/kirby/toolkit/bootstrap.php b/kirby/toolkit/bootstrap.php new file mode 100644 index 0000000..6aeb70a --- /dev/null +++ b/kirby/toolkit/bootstrap.php @@ -0,0 +1,97 @@ + __DIR__ . DS . 'lib' . DS . 'a.php', + 'bitmask' => __DIR__ . DS . 'lib' . DS . 'bitmask.php', + 'brick' => __DIR__ . DS . 'lib' . DS . 'brick.php', + 'c' => __DIR__ . DS . 'lib' . DS . 'c.php', + 'cookie' => __DIR__ . DS . 'lib' . DS . 'cookie.php', + 'cache' => __DIR__ . DS . 'lib' . DS . 'cache.php', + 'cache\\driver' => __DIR__ . DS . 'lib' . DS . 'cache' . DS . 'driver.php', + 'cache\\driver\\apc' => __DIR__ . DS . 'lib' . DS . 'cache' . DS . 'driver' . DS . 'apc.php', + 'cache\\driver\\file' => __DIR__ . DS . 'lib' . DS . 'cache' . DS . 'driver' . DS . 'file.php', + 'cache\\driver\\memcached' => __DIR__ . DS . 'lib' . DS . 'cache' . DS . 'driver' . DS . 'memcached.php', + 'cache\\driver\\mock' => __DIR__ . DS . 'lib' . DS . 'cache' . DS . 'driver' . DS . 'mock.php', + 'cache\\driver\\session' => __DIR__ . DS . 'lib' . DS . 'cache' . DS . 'driver' . DS . 'session.php', + 'cache\\value' => __DIR__ . DS . 'lib' . DS . 'cache' . DS . 'value.php', + 'collection' => __DIR__ . DS . 'lib' . DS . 'collection.php', + 'crypt' => __DIR__ . DS . 'lib' . DS . 'crypt.php', + 'data' => __DIR__ . DS . 'lib' . DS . 'data.php', + 'database' => __DIR__ . DS . 'lib' . DS . 'database.php', + 'database\\query' => __DIR__ . DS . 'lib' . DS . 'database' . DS . 'query.php', + 'db' => __DIR__ . DS . 'lib' . DS . 'db.php', + 'detect' => __DIR__ . DS . 'lib' . DS . 'detect.php', + 'dimensions' => __DIR__ . DS . 'lib' . DS . 'dimensions.php', + 'dir' => __DIR__ . DS . 'lib' . DS . 'dir.php', + 'email' => __DIR__ . DS . 'lib' . DS . 'email.php', + 'embed' => __DIR__ . DS . 'lib' . DS . 'embed.php', + 'error' => __DIR__ . DS . 'lib' . DS . 'error.php', + 'errorreporting' => __DIR__ . DS . 'lib' . DS . 'errorreporting.php', + 'escape' => __DIR__ . DS . 'lib' . DS . 'escape.php', + 'exif' => __DIR__ . DS . 'lib' . DS . 'exif.php', + 'exif\\camera' => __DIR__ . DS . 'lib' . DS . 'exif' . DS . 'camera.php', + 'exif\\location' => __DIR__ . DS . 'lib' . DS . 'exif' . DS . 'location.php', + 'f' => __DIR__ . DS . 'lib' . DS . 'f.php', + 'folder' => __DIR__ . DS . 'lib' . DS . 'folder.php', + 'header' => __DIR__ . DS . 'lib' . DS . 'header.php', + 'html' => __DIR__ . DS . 'lib' . DS . 'html.php', + 'i' => __DIR__ . DS . 'lib' . DS . 'i.php', + 'l' => __DIR__ . DS . 'lib' . DS . 'l.php', + 'media' => __DIR__ . DS . 'lib' . DS . 'media.php', + 'obj' => __DIR__ . DS . 'lib' . DS . 'obj.php', + 'pagination' => __DIR__ . DS . 'lib' . DS . 'pagination.php', + 'password' => __DIR__ . DS . 'lib' . DS . 'password.php', + 'r' => __DIR__ . DS . 'lib' . DS . 'r.php', + 'redirect' => __DIR__ . DS . 'lib' . DS . 'redirect.php', + 'remote' => __DIR__ . DS . 'lib' . DS . 'remote.php', + 'response' => __DIR__ . DS . 'lib' . DS . 'response.php', + 'router' => __DIR__ . DS . 'lib' . DS . 'router.php', + 's' => __DIR__ . DS . 'lib' . DS . 's.php', + 'server' => __DIR__ . DS . 'lib' . DS . 'server.php', + 'silo' => __DIR__ . DS . 'lib' . DS . 'silo.php', + 'sql' => __DIR__ . DS . 'lib' . DS . 'sql.php', + 'str' => __DIR__ . DS . 'lib' . DS . 'str.php', + 'system' => __DIR__ . DS . 'lib' . DS . 'system.php', + 'thumb' => __DIR__ . DS . 'lib' . DS . 'thumb.php', + 'timer' => __DIR__ . DS . 'lib' . DS . 'timer.php', + 'toolkit' => __DIR__ . DS . 'lib' . DS . 'toolkit.php', + 'tpl' => __DIR__ . DS . 'lib' . DS . 'tpl.php', + 'upload' => __DIR__ . DS . 'lib' . DS . 'upload.php', + 'url' => __DIR__ . DS . 'lib' . DS . 'url.php', + 'v' => __DIR__ . DS . 'lib' . DS . 'v.php', + 'visitor' => __DIR__ . DS . 'lib' . DS . 'visitor.php', + 'xml' => __DIR__ . DS . 'lib' . DS . 'xml.php', + 'yaml' => __DIR__ . DS . 'lib' . DS . 'yaml.php', + + // vendors + 'spyc' => __DIR__ . DS . 'vendors' . DS . 'yaml' . DS . 'yaml.php', + 'abeautifulsite\\simpleimage' => __DIR__ . DS . 'vendors' . DS . 'abeautifulsite' . DS . 'SimpleImage.php', + 'mimereader' => __DIR__ . DS . 'vendors' . DS . 'mimereader' . DS . 'mimereader.php', + +)); + +// load all helpers +include(__DIR__ . DS . 'helpers.php'); \ No newline at end of file diff --git a/kirby/toolkit/helpers.php b/kirby/toolkit/helpers.php new file mode 100644 index 0000000..cf39329 --- /dev/null +++ b/kirby/toolkit/helpers.php @@ -0,0 +1,353 @@ +' . print_r($variable, true) . ''; + } + if($echo === true) echo $output; + return $output; +} + +/** + * Generates a single attribute or a list of attributes + * + * @see html::attr(); + * @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 + */ +function attr($name, $value = null) { + return html::attr($name, $value); +} + +/** + * Creates safe html by encoding special characters + * + * @param string $text unencoded text + * @param bool $keepTags + * @return string + */ +function html($text, $keepTags = true) { + return html::encode($text, $keepTags); +} + +/** + * Shortcut for html() + * + * @see html() + * @param $text + * @param bool $keepTags + * @return string + */ +function h($text, $keepTags = true) { + return html::encode($text, $keepTags); +} + +/** + * Shortcut for xml::encode() + * + * @param $text + * @return string + */ +function xml($text) { + return xml::encode($text); +} + +/** + * 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('escape', $context)) { + return escape::$context($string, $strict); + } +} + +/** + * 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 + */ +function widont($string = '') { + return str::widont($string); +} + +/** + * Convert a text to multiline text + * + * @param string $text + * @return string + */ +function multiline($text) { + return nl2br(html($text)); +} + +/** + * Returns the memory usage in a readable format + * + * @return string + */ +function memory() { + return f::niceSize(memory_get_usage()); +} + +/** + * Determines the size/length of numbers, strings, arrays and files + * + * @param mixed $value + * @return int + */ +function size($value) { + if(is_numeric($value)) return $value; + if(is_string($value)) return str::length(trim($value)); + if(is_array($value)) return count($value); + if(f::exists($value)) return f::size($value) / 1024; +} + +/** + * Generates a gravatar image link + * + * @param string $email + * @param int $size + * @param string $default + * @return string + */ +function gravatar($email, $size = 256, $default = 'mm') { + return 'https://gravatar.com/avatar/' . md5(strtolower(trim($email))) . '?d=' . urlencode($default) . '&s=' . $size; +} + +/** + * Checks / returns a csrf token + * + * @param string $check Pass a token here to compare it to the one in the session + * @return mixed Either the token or a boolean check result + */ +function csrf($check = null) { + + // make sure a session is started + s::start(); + + if(is_null($check)) { + $token = str::random(64); + s::set('csrf', $token); + return $token; + } + + return ($check === s::get('csrf')) ? true : false; + +} + +/** + * Facepalm typo alias + * @see csrf() + */ +function csfr($check = null) { + return csrf($check); +} + +/** + * Shortcut for call_user_func_array with a better handling of arguments + * + * @param mixed $function + * @param mixed $arguments + * @return mixed + */ +function call($function, $arguments = array()) { + if(!is_callable($function)) return false; + if(!is_array($arguments)) $arguments = array($arguments); + return call_user_func_array($function, $arguments); +} + +/** + * Parses yaml structured text + * + * @param $string + * @return array + */ +function yaml($string) { + return yaml::decode($string); +} + +/** + * Simple email sender helper + * + * @param array $params + * @return Email + */ +function email($params = array()) { + return new Email($params); +} + +/** + * Shortcut for the upload class + * + * @param $to + * @param array $params + * @return Upload + */ +function upload($to, $params = array()) { + return new Upload($to, $params); +} + +/** + * Checks for invalid data + * + * @param array $data + * @param array $rules + * @param array $messages + * @return mixed + */ +function invalid($data, $rules, $messages = array()) { + $errors = array(); + foreach($rules as $field => $validations) { + foreach($validations as $method => $options) { + if(is_numeric($method)) $method = $options; + if($method == 'required') { + if(!isset($data[$field]) || (empty($data[$field]) && $data[$field] !== 0)) { + $errors[$field] = a::get($messages, $field, $field); + } + } else if(!empty($data[$field]) || $data[$field] === 0) { + if(!is_array($options)) $options = array($options); + array_unshift($options, a::get($data, $field)); + if(!call(array('v', $method), $options)) { + $errors[$field] = a::get($messages, $field, $field); + } + } + } + } + return array_unique($errors); +} + + +/** + * Shortcut for the language variable getter + * + * @param string $key + * @param mixed $default + * @return string + */ +function l($key, $default = null) { + return l::get($key, $default); +} + +/** + * @param $tag + * @param bool $html + * @param array $attr + * @return Brick + */ +function brick($tag, $html = false, $attr = array()) { + return new Brick($tag, $html, $attr); +} diff --git a/kirby/toolkit/lib/a.php b/kirby/toolkit/lib/a.php new file mode 100644 index 0000000..309649f --- /dev/null +++ b/kirby/toolkit/lib/a.php @@ -0,0 +1,495 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class A { + + /** + * Gets an element of an array by key + * + * + * + * $array = 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: array( + * // 'cat' => 'miao', + * // 'dog' => 'wuff' + * // ); + * + * + * + * @param array $array The source array + * @param mixed $key The key to look for + * @param mixed $default Optional default value, which should be returned if no element has been found + * @return mixed + */ + public static function get($array, $key, $default = null) { + + // get an array of keys + if(is_array($key)) { + $result = array(); + foreach($key as $k) $result[$k] = static::get($array, $k); + return $result; + + // get a single + } else if(isset($array[$key])) { + return $array[$key]; + + // return the entire array if the key is null + } else if(is_null($key)) { + return $array; + + // get the default value if nothing else worked out + } else { + return $default; + } + + } + + /** + * Shows an entire array or object in a human readable way + * This is perfect for debugging + * + * + * + * $array = array( + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ); + * + * a::show($array); + * + * // output: + * // Array + * // ( + * // [cat] => miao + * // [dog] => wuff + * // [bird] => tweet + * // ) + * + * + * + * @param array $array The source array + * @param boolean $echo By default the result will be echoed instantly. You can switch that off here. + * @return mixed If echo is false, this will return the generated array output. + */ + public static function show($array, $echo = true) { + return dump($array, $echo); + } + + /** + * Converts an array to a JSON string + * It's basically a shortcut for json_encode() + * + * + * + * $array = array( + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ); + * + * echo a::json($array); + * // output: {"cat":"miao","dog":"wuff","bird":"tweet"} + * + * + * + * @param array $array The source array + * @return string The JSON string + */ + public static function json($array) { + return json_encode((array)$array); + } + + /** + * Converts an array to a XML string + * + * + * + * $array = array( + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ); + * + * echo a::xml($array, 'animals'); + * // output: + * // + * // miao + * // wuff + * // tweet + * // + * + * + * + * @param array $array The source array + * @param string $tag The name of the root element + * @param boolean $head Include the xml declaration head or not + * @param string $charset The charset, which should be used for the header + * @param int $level The indendation level + * @return string The XML string + */ + public static function xml($array, $tag = 'root', $head = true, $charset = 'utf-8', $tab = ' ', $level = 0) { + return xml::create($array, $tag, $head, $charset, $tab, $level); + } + + /** + * Extracts a single column from an array + * + * + * + * $array[0] = array( + * 'id' => 1, + * 'username' => 'bastian', + * ); + * + * $array[1] = array( + * 'id' => 2, + * 'username' => 'peter', + * ); + * + * $array[3] = array( + * 'id' => 3, + * 'username' => 'john', + * ); + * + * $extract = a::extract($array, 'username'); + * + * // result: array( + * // 'bastian', + * // 'peter', + * // 'john' + * // ); + * + * + * + * @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 extract($array, $key) { + $output = array(); + foreach($array AS $a) if(isset($a[$key])) $output[] = $a[ $key ]; + return $output; + } + + /** + * Shuffles an array and keeps the keys + * + * + * + * $array = array( + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ); + * + * $shuffled = a::shuffle($array); + * // output: array( + * // 'dog' => 'wuff', + * // 'cat' => 'miao', + * // 'bird' => 'tweet' + * // ); + * + * + * + * @param array $array The source array + * @return array The shuffled result array + */ + public static function shuffle($array) { + + $keys = array_keys($array); + $new = array(); + + shuffle($keys); + + // resort the array + foreach($keys as $key) $new[$key] = $array[$key]; + return $new; + + } + + /** + * Returns the first element of an array + * + * I always have to lookup the names of that function + * so I decided to make this shortcut which is + * easier to remember. + * + * + * + * $array = array( + * 'cat', + * 'dog', + * 'bird', + * ); + * + * $first = a::first($array); + * // first: 'cat' + * + * + * + * @param array $array The source array + * @return mixed The first element + */ + public static function first($array) { + return array_shift($array); + } + + /** + * Returns the last element of an array + * + * I always have to lookup the names of that function + * so I decided to make this shortcut which is + * easier to remember. + * + * + * + * $array = array( + * 'cat', + * 'dog', + * 'bird', + * ); + * + * $last = a::last($array); + * // first: 'bird' + * + * + * + * @param array $array The source array + * @return mixed The last element + */ + public static function last($array) { + return array_pop($array); + } + + /** + * Fills an array up with additional elements to certain amount. + * + * + * + * $array = array( + * 'cat', + * 'dog', + * 'bird', + * ); + * + * $result = a::fill($array, 5, 'elephant'); + * + * // result: array( + * // '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, $limit, $fill='placeholder') { + if(count($array) < $limit) { + $diff = $limit-count($array); + for($x=0; $x<$diff; $x++) $array[] = $fill; + } + 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 = array( + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ); + * + * $required = array('cat', 'elephant'); + * + * $missng = a::missing($array, $required); + * // missing: array( + * // '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, $required=array()) { + $missing = array(); + foreach($required AS $r) { + if(empty($array[$r])) $missing[] = $r; + } + return $missing; + } + + /** + * Sorts a multi-dimensional array by a certain column + * + * + * + * $array[0] = array( + * 'id' => 1, + * 'username' => 'bastian', + * ); + * + * $array[1] = array( + * 'id' => 2, + * 'username' => 'peter', + * ); + * + * $array[3] = array( + * 'id' => 3, + * 'username' => 'john', + * ); + * + * $sorted = a::sort($array, 'username ASC'); + * // Array + * // ( + * // [0] => Array + * // ( + * // [id] => 1 + * // [username] => bastian + * // ) + * // [1] => Array + * // ( + * // [id] => 3 + * // [username] => john + * // ) + * // [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 const $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, $field, $direction = 'desc', $method = SORT_REGULAR) { + + $direction = strtolower($direction) == 'desc' ? SORT_DESC : SORT_ASC; + $helper = array(); + $result = array(); + + // build the helper array + foreach($array as $key => $row) $helper[$key] = $row[$field]; + + // natural sorting + if($method === SORT_NATURAL) { + natsort($helper); + if($direction === SORT_DESC) $helper = array_reverse($helper); + } else 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 (experimental) + * + * @param array $array The array to analyze + * @return boolean true: The array is associative false: It's not + */ + public static function isAssociative($array) { + return !ctype_digit(implode(NULL, array_keys($array))); + } + + /** + * Returns the average value of an array + * + * @param array $array The source array + * @param int $decimals The number of decimals to return + * @return int The average value + */ + public static function average($array, $decimals = 0) { + return round(array_sum($array), $decimals) / sizeof($array); + } + + /** + * Merges arrays recursively + * + * @param array $array1 + * @param array $array2 + * @return array + */ + public static function merge($array1, $array2) { + $merged = $array1; + foreach($array2 as $key => $value) { + if(is_array($value) && isset($merged[$key]) && is_array($merged[$key])) { + $merged[$key] = static::merge($merged[$key], $value); + } else { + $merged[$key] = $value; + } + } + return $merged; + } + + /** + * Update an array with a second array + * The second array can contain callbacks as values, + * which will get the original values as argument + * + * @param array $array + * @param array $update + */ + public static function update($array, $update) { + + foreach($update as $key => $value) { + if(is_a($value, 'Closure')) { + $array[$key] = call($value, static::get($array, $key)); + } else { + $array[$key] = $value; + } + } + + return $array; + + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/bitmask.php b/kirby/toolkit/lib/bitmask.php new file mode 100644 index 0000000..db5e7f7 --- /dev/null +++ b/kirby/toolkit/lib/bitmask.php @@ -0,0 +1,75 @@ + + * @link http://getkirby.com + * @copyright Lukas Bestle + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Bitmask { + + /** + * Checks if a value can be used as bitmask value (checks for a power of two) + * + * @param mixed $value The value to check for + * @return boolean + */ + public static function validValue($value) { + return is_int($value) && ($value & ($value - 1)) == 0; + } + + /** + * Checks if a bitmask includes a value + * + * @param int $value The value to check for + * @param int $bitmask The bitmask to check in + * @return boolean + */ + public static function includes($value, $bitmask) { + if(!static::validValue($value)) return false; + + return ($bitmask & $value) !== 0; + } + + /** + * Adds a value to a bitmask + * + * @param int $value The value to add + * @param int $bitmask The bitmask to add the value to + * @return int + */ + public static function add($value, $bitmask) { + if(!static::validValue($value)) { + throw new Exception('The value "' . $value . '" is not appropriate for usage in bitmasks.'); + } + + // check if the bitmask already includes the value + if(static::includes($value, $bitmask)) return $bitmask; + + return $bitmask | $value; + } + + /** + * Removes a value from a bitmask + * + * @param int $value The value to remove + * @param int $bitmask The bitmask to remove the value from + * @return int + */ + public static function remove($value, $bitmask) { + if(!static::validValue($value)) { + throw new Exception('The value "' . $value . '" is not appropriate for usage in bitmasks.'); + } + + // check if the bitmask even includes the value + if(!static::includes($value, $bitmask)) return $bitmask; + + return $bitmask ^ $value; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/brick.php b/kirby/toolkit/lib/brick.php new file mode 100644 index 0000000..e83e97f --- /dev/null +++ b/kirby/toolkit/lib/brick.php @@ -0,0 +1,204 @@ +tag($tag); + $this->html($html); + $this->attr($attr); + + } + + public function __set($attr, $value) { + $this->attr($attr, $value); + } + + public function on($event, $callback) { + if(!isset($this->events[$event])) $this->events[$event] = array(); + $this->events[$event][] = $callback; + return $this; + } + + public function trigger($event, $args = array()) { + if(isset($this->events[$event])) { + array_unshift($args, $this); + foreach($this->events[$event] as $e) { + call_user_func_array($e, $args); + } + } + } + + public function tag($tag = null) { + if(is_null($tag)) return $this->tag; + $this->tag = $tag; + return $this; + } + + public function attr($key = null, $value = null) { + if(is_null($key)) { + return $this->attr; + } else if(is_array($key)) { + foreach($key as $k => $v) { + $this->attr($k, $v); + } + return $this; + } else if(is_null($value)) { + return a::get($this->attr, $key); + } else if($key == 'class') { + $this->addClass($value); + return $this; + } else { + $this->attr[$key] = $value; + return $this; + } + } + + public function data($key = null, $value = null) { + if(is_null($key)) { + $data = array(); + foreach($this->attr as $key => $val) { + if(str::startsWith($key, 'data-')) { + $data[$key] = $val; + } + } + return $data; + } else if(is_array($key)) { + foreach($key as $k => $v) { + $this->data($k, $v); + } + return $this; + } else if(is_null($value)) { + return a::get($this->attr, 'data-' . $key); + } else { + $this->attr['data-' . $key] = $value; + return $this; + } + } + + + public function removeAttr($key) { + unset($this->attr[$key]); + } + + public function classNames() { + + if(!isset($this->attr['class'])) { + $this->attr['class'] = array(); + } else if(is_string($this->attr['class'])) { + $raw = $this->attr['class']; + $this->attr['class'] = array(); + $this->addClass($raw); + } + + return $this->attr['class']; + + } + + public function val($value = null) { + return $this->attr('value', $value); + } + + public function addClass($class) { + + $classNames = $this->classNames(); + $classIndex = array_map('strtolower', $classNames); + + foreach(str::split($class, ' ') as $c) { + if(!in_array(strtolower($c), $classIndex)) { + $classNames[] = $c; + } + } + + $this->attr['class'] = $classNames; + + return $this; + + } + + public function removeClass($class) { + + $classNames = $this->classNames(); + + foreach(str::split($class, ' ') as $c) { + $classNames = array_filter($classNames, function($e) use($c) { + return (strtolower($e) !== strtolower($c)); + }); + } + + $this->attr['class'] = $classNames; + + return $this; + + } + + public function replaceClass($classA, $classB) { + return $this->removeClass($classA)->addClass($classB); + } + + public function text($text = null) { + if(is_null($text)) return trim(strip_tags($this->html)); + $this->html = html($text, false); + return $this; + } + + public function html($html = null) { + if(is_null($html)) { + return $this->html = $this->isVoid() ? null : $this->html; + } + $this->html = $html; + return $this; + } + + public function prepend($html) { + if(is_callable($html)) $html = $html(); + $this->html = $html . $this->html; + return $this; + } + + public function append($html) { + if(is_callable($html)) $html = $html(); + $this->html = $this->html . $html; + return $this; + } + + public function isVoid() { + return html::isVoid($this->tag()); + } + + public function toString() { + $this->attr['class'] = implode(' ', $this->classNames()); + return html::tag($this->tag(), $this->html(), $this->attr()); + } + + public function __toString() { + try { + return $this->toString(); + } catch(Exception $e) { + return 'Error: ' . $e->getMessage(); + } + } + + public static function make($id, $callback) { + static::$bricks[$id] = $callback; + } + + public static function get($id) { + if(!isset(static::$bricks[$id])) return false; + $args = array_slice(func_get_args(), 1); + return call_user_func_array(static::$bricks[$id], $args); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/c.php b/kirby/toolkit/lib/c.php new file mode 100644 index 0000000..cdb9b73 --- /dev/null +++ b/kirby/toolkit/lib/c.php @@ -0,0 +1,17 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class C extends Silo { + public static $data = array(); +} \ No newline at end of file diff --git a/kirby/toolkit/lib/cache.php b/kirby/toolkit/lib/cache.php new file mode 100644 index 0000000..6f449e4 --- /dev/null +++ b/kirby/toolkit/lib/cache.php @@ -0,0 +1,59 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Cache { + + const ERROR_INVALID_DRIVER = 0; + const ERROR_INVALID_DRIVER_INSTANCE = 1; + const ERROR_UNKNOWN_METHOD = 2; + + public static $driver = null; + + /** + * Setup simplifier for the current driver + * + * @param string $driver + * @param mixed $args + * @return Cache\Driver + */ + public static function setup($driver, $args = null) { + $ref = new ReflectionClass('Cache\\Driver\\' . $driver); + return static::$driver = $ref->newInstanceArgs(array($args)); + } + + /** + * Accessor for all static driver methods + * + * @param string $method + * @param mixed $args + * @return mixed + */ + public static function __callStatic($method, $args) { + + if(is_null(static::$driver)) { + throw new Error('Please define a cache driver', static::ERROR_INVALID_DRIVER); + } + + if(!is_a(static::$driver, 'Cache\\Driver')) { + throw new Error('The cache driver must be an instance of the Cache\\Driver class', static::ERROR_INVALID_DRIVER_INSTANCE); + } + + if(method_exists(static::$driver, $method)) { + return call(array(static::$driver, $method), $args); + } else { + throw new Error('Invalid cache method: ' . $method, static::ERROR_UNKNOWN_METHOD); + } + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/cache/driver.php b/kirby/toolkit/lib/cache/driver.php new file mode 100755 index 0000000..be6f4ae --- /dev/null +++ b/kirby/toolkit/lib/cache/driver.php @@ -0,0 +1,190 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +abstract class Driver { + + // stores all options for the driver + protected $options = array(); + + /** + * Set all parameters which are needed to connect to the cache storage + * + * @param array $params + */ + public function __construct($params = array()) {} + + /** + * 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 abstract function set($key, $value, $minutes = null); + + /** + * Private method to retrieve the cache value + * This needs to be defined by the driver + * + * @param string $key + * @return object Value + */ + public abstract function retrieve($key); + + /** + * 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($key, $default = null) { + + // get the Value + $value = $this->retrieve($key); + + // check for a valid cache value + if(!is_a($value, '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 (!is_null($cache)) ? $cache : $default; + + } + + /** + * Calculates the expiration timestamp + * + * @param int $minutes + * @return int + */ + protected function expiration($minutes = null) { + // keep forever if minutes are not defined + if(is_null($minutes)) $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($key) { + // get the Value object + $value = $this->retrieve($key); + + // check for a valid Value object + if(!is_a($value, 'Cache\\Value')) return false; + + // return the expires timestamp + return $value->expires(); + } + + /** + * Checks if an item in the cache is expired + * + * @param string $key + * @return int + */ + public function expired($key) { + return $this->expires($key) <= time(); + } + + /** + * Checks when the cache has been created + * + * @param string $key + * @return mixed + */ + public function created($key) { + // get the Value object + $value = $this->retrieve($key); + + // check for a valid Value object + if(!is_a($value, 'Cache\\Value')) return false; + + // return the expires timestamp + return $value->created(); + } + + /** + * Alternate version for cache::created($key) + */ + public function modified($key) { + return static::created($key); + } + + /** + * An array with value, created timestamp and expires timestamp + * + * @param mixed $value The value, which should be cached + * @param int $minutes The number of minutes before expiration + * @return array + */ + protected function value($value, $minutes) { + return new Value($value, $minutes); + } + + /** + * Determine if an item exists in the cache. + * + * @param string $key + * @return boolean + */ + public function exists($key) { + return !$this->expired($key); + } + + /** + * Remove an item from the cache + * + * @param string $key + * @return boolean + */ + public abstract function remove($key); + + /** + * Flush the entire cache + * + * @return boolean + */ + public abstract function flush(); + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/cache/driver/apc.php b/kirby/toolkit/lib/cache/driver/apc.php new file mode 100644 index 0000000..ec7603b --- /dev/null +++ b/kirby/toolkit/lib/cache/driver/apc.php @@ -0,0 +1,74 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Apc extends Driver { + + /** + * 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($key, $value, $minutes = null) { + return apc_store($key, $this->value($value, $minutes), $this->expiration($minutes)); + } + + /** + * Retrieve an item from the cache. + * + * @param string $key + * @return mixed + */ + public function retrieve($key) { + return apc_fetch($key); + } + + /** + * Checks if the current key exists in cache + * + * @param string $key + * @return boolean + */ + public function exists($key) { + return apc_exists($key); + } + + /** + * Remove an item from the cache + * + * @param string $key + * @return boolean + */ + public function remove($key) { + return apc_delete($key); + } + + /** + * Flush the entire cache directory + * + * @return boolean + */ + public function flush() { + return apc_clear_cache('user'); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/cache/driver/file.php b/kirby/toolkit/lib/cache/driver/file.php new file mode 100644 index 0000000..7563bd4 --- /dev/null +++ b/kirby/toolkit/lib/cache/driver/file.php @@ -0,0 +1,121 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class File extends Driver { + + const ERROR_MISSING_CACHE_DIRECTORY = 0; + + /** + * Set all parameters which are needed for the file cache + * see defaults for available parameters + * + * @param array $params + */ + public function __construct($params = array()) { + + if(is_string($params)) { + $params = array('root' => $params); + } + + $defaults = array( + 'root' => null, + 'extension' => null + ); + + $this->options = array_merge($defaults, $params); + + // check for a valid cache directory + if(!is_dir($this->options['root'])) { + throw new Error('The cache directory does not exist', static::ERROR_MISSING_CACHE_DIRECTORY); + } + + } + + /** + * Returns the full path to a file for a given key + * + * @param string $key + * @return string + */ + protected function file($key) { + return $this->options['root'] . DS . $key . r($this->options['extension'], '.' . $this->options['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 + * @return void + */ + public function set($key, $value, $minutes = null) { + return f::write($this->file($key), serialize($this->value($value, $minutes))); + } + + /** + * Retrieve an item from the cache. + * + * @param string $key + * @return object CacheValue + */ + public function retrieve($key) { + // unserialized value array (see $this->value()) + return unserialize(f::read($this->file($key))); + } + + /** + * Checks when the cache has been created + * + * @param string $key + * @return int + */ + public function created($key) { + // use the modification timestamp + // as indicator when the cache has been created/overwritten + clearstatcache(); + // get the file for this cache key + $file = $this->file($key); + return file_exists($file) ? filemtime($this->file($key)) : 0; + } + + /** + * Remove an item from the cache + * + * @param string $key + * @return boolean + */ + public function remove($key) { + return f::remove($this->file($key)); + } + + /** + * Flush the entire cache directory + * + * @return boolean + */ + public function flush() { + return dir::clean($this->options['root']); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/cache/driver/memcached.php b/kirby/toolkit/lib/cache/driver/memcached.php new file mode 100644 index 0000000..8320467 --- /dev/null +++ b/kirby/toolkit/lib/cache/driver/memcached.php @@ -0,0 +1,128 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Memcached extends Driver { + + // store for the memache connection + protected $connection = null; + + /** + * Set all parameters which are needed for the memcache client + * see defaults for available parameters + * + * @param array $params + */ + public function __construct($params = array()) { + + $defaults = array( + 'host' => 'localhost', + 'port' => 11211, + 'prefix' => null, + ); + + $this->options = array_merge($defaults, (array)$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($key, $value, $minutes = null) { + return $this->connection->set($this->key($key), $this->value($value, $minutes), $this->expiration($minutes)); + } + + /** + * Returns the full keyname + * including the prefix (if set) + * + * @param string $key + * @return string + */ + public function key($key) { + return $this->options['prefix'] . $key; + } + + /** + * Retrieve the CacheValue object from the cache. + * + * @param string $key + * @return object CacheValue + */ + public function retrieve($key) { + return $this->connection->get($this->key($key)); + } + + /** + * Remove an item from the cache + * + * @param string $key + * @return boolean + */ + public function remove($key) { + return $this->connection->delete($this->key($key)); + } + + /** + * Checks when an item in the cache expires + * + * @param string $key + * @return int + */ + public function expires($key) { + return parent::expires($this->key($key)); + } + + /** + * Checks if an item in the cache is expired + * + * @param string $key + * @return int + */ + public function expired($key) { + return parent::expired($this->key($key)); + } + + /** + * Checks when the cache has been created + * + * @param string $key + * @return int + */ + public function created($key) { + return parent::created($this->key($key)); + } + + /** + * Flush the entire cache directory + * + * @return boolean + */ + public function flush() { + return $this->connection->flush(); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/cache/driver/mock.php b/kirby/toolkit/lib/cache/driver/mock.php new file mode 100644 index 0000000..0c95484 --- /dev/null +++ b/kirby/toolkit/lib/cache/driver/mock.php @@ -0,0 +1,74 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Mock extends Driver { + + /** + * 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($key, $value, $minutes = null) { + return true; + } + + /** + * Retrieve an item from the cache. + * + * @param string $key + * @return mixed + */ + public function retrieve($key) { + return null; + } + + /** + * Checks if the current key exists in cache + * + * @param string $key + * @return boolean + */ + public function exists($key) { + return null; + } + + /** + * Remove an item from the cache + * + * @param string $key + * @return boolean + */ + public function remove($key) { + return true; + } + + /** + * Flush the entire cache directory + * + * @return boolean + */ + public function flush() { + return true; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/cache/driver/session.php b/kirby/toolkit/lib/cache/driver/session.php new file mode 100644 index 0000000..5971c8f --- /dev/null +++ b/kirby/toolkit/lib/cache/driver/session.php @@ -0,0 +1,78 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Session extends Driver { + + /** + * Make sure the session is started within the constructor + */ + public function __construct() { + s::start(); + if(!isset($_SESSION['_cache'])) { + $_SESSION['_cache'] = array(); + } + } + + /** + * 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($key, $value, $minutes = null) { + return $_SESSION['_cache'][$key] = $this->value($value, $minutes); + } + + /** + * Retrieve an item from the cache. + * + * @param string $key + * @return object CacheValue + */ + public function retrieve($key) { + return a::get($_SESSION['_cache'], $key); + } + + /** + * Remove an item from the cache + * + * @param string $key + * @return boolean + */ + public function remove($key) { + unset($_SESSION['_cache'][$key]); + } + + /** + * Flush the entire cache directory + * + * @return boolean + */ + public function flush() { + $_SESSION['_cache'] = array(); + return true; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/cache/value.php b/kirby/toolkit/lib/cache/value.php new file mode 100644 index 0000000..f2bfc8a --- /dev/null +++ b/kirby/toolkit/lib/cache/value.php @@ -0,0 +1,75 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Value { + + // the cached value + protected $value; + + // the expiration timestamp + protected $expires; + + // the creation timestamp + protected $created; + + /** + * Constructor + * + * @param mixed $value + * @param int $minutes the number of minutes until the value expires + */ + public function __construct($value, $minutes = null) { + + // keep forever if minutes are not defined + if(is_null($minutes)) $minutes = 2628000; + + // take the current time + $time = time(); + + $this->value = $value; + $this->expires = $time + ($minutes * 60); + $this->created = $time; + + } + + /** + * Returns the value + * + * @return mixed + */ + public function value() { + return $this->value; + } + + /** + * Returns the expiration date as UNIX timestamp + * + * @return int + */ + public function expires() { + return $this->expires; + } + + /** + * Returns the creation date as UNIX timestamp + * + * @return int + */ + public function created() { + return $this->created; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/collection.php b/kirby/toolkit/lib/collection.php new file mode 100644 index 0000000..79b88af --- /dev/null +++ b/kirby/toolkit/lib/collection.php @@ -0,0 +1,652 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Collection extends I { + + public static $filters = array(); + + protected $pagination; + + /** + * Returns a slice of the collection + * + * @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($offset = null, $limit = null) { + if($offset === null && $limit === null) return $this; + $collection = clone $this; + $collection->data = array_slice($collection->data, $offset, $limit); + return $collection; + } + + /** + * Returns a new combined collection + * + * @return Collection + */ + + public function merge($collection2) { + $collection = clone $this; + $collection->data = a::merge($collection->data, $collection2->data); + return $collection; + } + + /** + * Returns a new collection with a limited number of elements + * + * @param int $limit The number of elements to return + * @return Collection + */ + public function limit($limit) { + return $this->slice(0, $limit); + } + + /** + * Returns a new collection starting from the given offset + * + * @param int $offset The index to start from + * @return Collection + */ + public function offset($offset) { + return $this->slice($offset); + } + + /** + * Returns the array in reverse order + * + * @return Collection + */ + public function flip() { + $collection = clone $this; + $collection->data = array_reverse($collection->data, true); + return $collection; + } + + /** + * Counts all elements in the array + * + * @return int + */ + public function count() { + return count($this->data); + } + + /** + * Returns the first element from the array + * + * @return mixed + */ + public function first() { + $array = $this->data; + return array_shift($array); + } + + /** + * Checks if an element is in the collection by key. + * + * @param string $key + * @return boolean + */ + public function has($key) { + return isset($this->data[$key]); + } + + /** + * Returns the last element from the array + * + * @return mixed + */ + public function last() { + $array = $this->data; + return array_pop($array); + } + + /** + * Returns the nth element from the array + * + * @return mixed + */ + public function nth($n) { + $array = array_values($this->data); + return (isset($array[$n])) ? $array[$n] : false; + } + + /** + * Converts the current object into an array + * + * @return array + */ + public function toArray($callback = null) { + if(is_null($callback)) return $this->data; + return array_map($callback, $this->data); + } + + /** + * Converts the current object into a json string + * + * @return string + */ + public function toJson() { + return json_encode($this->data); + } + + /** + * Appends an element to the data array + * + * @param string $key + * @param mixed $object + * @return Collection + */ + public function append($key, $object) { + $this->data = $this->data + array($key => $object); + return $this; + } + + /** + * Prepends an element to the data array + * + * @param string $key + * @param mixed $object + * @return Collection + */ + public function prepend($key, $object) { + $this->data = array($key => $object) + $this->data; + return $this; + } + + /** + * Returns a new collection without the given element(s) + * + * @param args any number of keys, passed as individual arguments + * @return Collection + */ + public function not() { + $collection = clone $this; + foreach(func_get_args() as $kill) { + unset($collection->data[$kill]); + } + return $collection; + } + + /** + * Returns a new collection without the given element(s) + * + * @param args any number of keys, passed as individual arguments + * @return Collection + */ + public function without() { + return call_user_func_array(array($this, 'not'), func_get_args()); + } + + /** + * Shuffle all elements in the array + * + * @return object a new shuffled collection + */ + public function shuffle() { + $collection = clone $this; + $keys = array_keys($collection->data); + shuffle($keys); + $collection->data = array_merge(array_flip($keys), $collection->data); + return $collection; + } + + /** + * Returns an array of all keys in the collection + * + * @return array + */ + public function keys() { + return array_keys($this->data); + } + + /** + * Tries to find the key for the given element + * + * @param mixed $needle the element to search for + * @return mixed the name of the key or false + */ + public function keyOf($needle) { + return array_search($needle, $this->data); + } + + /** + * Tries to find the index number for the given element + * + * @param mixed $needle the element to search for + * @return mixed the name of the key or false + */ + public function indexOf($needle) { + return array_search($needle, array_values($this->data)); + } + + /** + * Filter the elements in the array by a callback function + * + * @param func $callback the callback function + * @return Collection + */ + public function filter($callback) { + $collection = clone $this; + $collection->data = array_filter($collection->data, $callback); + return $collection; + } + + /** + * Find a single item by a key and value pair + * + * @param string $key + * @param mixed $value + * @return mixed + */ + public function findBy($key, $value) { + foreach($this->data as $item) { + if($this->extractValue($item, $key) == $value) return $item; + } + } + + /** + * Filters the current collection by a field, operator and search value + * + * @return Collection + */ + public function filterBy() { + + $args = func_get_args(); + $operator = '=='; + $field = @$args[0]; + $value = @$args[1]; + $split = @$args[2]; + $collection = clone $this; + + if(is_string($value) && array_key_exists($value, static::$filters)) { + $operator = $value; + $value = @$args[2]; + $split = @$args[3]; + } + + if(is_object($value)) { + $value = (string)$value; + } + + if(array_key_exists($operator, static::$filters)) { + + $collection = call_user_func_array(static::$filters[$operator], array( + $collection, + $field, + $value, + $split + )); + + } + + return $collection; + + } + + /** + * Makes sure to provide a valid value for each filter method + * no matter if an object or an array is given + * + * @param mixed $item + * @param string $field + * @return mixed + */ + static public function extractValue($item, $field) { + if(is_array($item) && isset($item[$field])) { + return $item[$field]; + } else if(is_object($item)) { + return $item->$field(); + } else { + return false; + } + } + + /** + * Sorts the collection by any number of fields + * + * @return Collection + */ + public function sortBy() { + + $args = func_get_args(); + $collection = clone $this; + $array = $collection->data; + $params = array(); + + if(empty($array)) return $collection; + + foreach($args as $i => $param) { + if(is_string($param)) { + if(strtolower($param) === 'desc') { + ${"param_$i"} = SORT_DESC; + } else if(strtolower($param) === 'asc') { + ${"param_$i"} = SORT_ASC; + } else { + ${"param_$i"} = array(); + foreach($array as $index => $row) { + ${"param_$i"}[$index] = is_array($row) ? str::lower($row[$param]) : str::lower($row->$param()); + } + } + } else { + ${"param_$i"} = $args[$i]; + } + $params[] = &${"param_$i"}; + } + + $params[] = &$array; + + call_user_func_array('array_multisort', $params); + + $collection->data = $array; + + return $collection; + + } + + /** + * Add pagination + * + * @param int $limit the number of items per page + * @param array $options and optional array with options for the pagination class + * @return object a sliced set of data + */ + public function paginate($limit, $options = array()) { + + if(is_a($limit, 'Pagination')) { + $this->pagination = $limit; + return $this; + } + + $pagination = new Pagination($this->count(), $limit, $options); + $pages = $this->slice($pagination->offset(), $pagination->limit()); + $pages->pagination = $pagination; + + return $pages; + + } + + /** + * Get the previously added pagination object + * + * @return object + */ + public function pagination() { + return $this->pagination; + } + + /** + * Map a function to each item in the collection + * + * @param function $callback + * @return Collection + */ + public function map($callback) { + $this->data = array_map($callback, $this->data); + return $this; + } + + /** + * Extracts all values for a single field into + * a new array + * + * @param string $field + * @return array + */ + public function pluck($field, $split = null, $unique = false) { + + $result = array(); + + foreach($this->data as $item) { + $row = $this->extractValue($item, $field); + + if($split) { + $result = array_merge($result, str::split($row, $split)); + } else { + $result[] = $row; + } + + } + + if($unique) { + $result = array_unique($result); + } + + return array_values($result); + + } + + /** + * Groups the collection by a given callback + * + * @param callable $callback + * @return object A new collection with an item for each group and a subcollection in each group + */ + public function group($callback) { + + if (!is_callable($callback)) throw new Exception($callback . ' is not callable. Did you mean to use groupBy()?'); + + $groups = array(); + + foreach($this->data as $key => $item) { + + // get the value to group by + $value = call_user_func($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)) { + throw new Exception('You cannot group by arrays or objects'); + } else if(is_object($value)) { + if(!method_exists($value, '__toString')) { + throw new Exception('You cannot group by arrays or objects'); + } else { + $value = (string)$value; + } + } + + if(!isset($groups[$value])) { + // create a new entry for the group if it does not exist yet + $groups[$value] = new static(array($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 + * @return object A new collection with an item for each group and a subcollection in each group + */ + public function groupBy($field, $i = true) { + + if (!is_string($field)) 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->extractValue($item, $field); + + // ignore upper/lowercase for group names + return ($i == true) ? str::lower($value) : $value; + + }); + + } + + public function set($key, $value) { + if(is_array($key)) { + $this->data = array_merge($this->data, $key); + return $this; + } + $this->data[$key] = $value; + return $this; + } + + public function __set($key, $value) { + $this->set($key, $value); + } + + public function get($key, $default = null) { + if(isset($this->data[$key])) { + return $this->data[$key]; + } else { + $lowerkeys = array_change_key_case($this->data, CASE_LOWER); + if(isset($lowerkeys[strtolower($key)])) { + return $lowerkeys[$key]; + } else { + return $default; + } + } + } + + public function __get($key) { + return $this->get($key); + } + + public function __call($key, $arguments) { + return $this->get($key); + } + + /** + * Makes it possible to echo the entire object + * + * @return string + */ + public function __toString() { + return implode('
', array_map(function($item) { + return (string)$item; + }, $this->data)); + } + +} + + +/** + * Add all available collection filters + * Those can be extended by creating your own: + * collection::$filters['your operator'] = function($collection, $field, $value, $split = false) { + * // your filter code + * }; + */ + +// take all matching elements +collection::$filters['=='] = function($collection, $field, $value, $split = false) { + + foreach($collection->data as $key => $item) { + + if($split) { + $values = str::split((string)collection::extractValue($item, $field), $split); + if(!in_array($value, $values)) unset($collection->$key); + } else if(collection::extractValue($item, $field) != $value) { + unset($collection->$key); + } + + } + + return $collection; + +}; + +// take all elements that won't match +collection::$filters['!='] = function($collection, $field, $value, $split = false) { + + foreach($collection->data as $key => $item) { + if($split) { + $values = str::split((string)collection::extractValue($item, $field), $split); + if(in_array($value, $values)) unset($collection->$key); + } else if(collection::extractValue($item, $field) == $value) { + unset($collection->$key); + } + } + + return $collection; + +}; + +// take all elements that partly match +collection::$filters['*='] = function($collection, $field, $value, $split = false) { + + foreach($collection->data as $key => $item) { + if($split) { + $values = str::split((string)collection::extractValue($item, $field), $split); + foreach($values as $val) { + if(strpos($val, $value) === false) { + unset($collection->$key); + break; + } + } + } else if(strpos(collection::extractValue($item, $field), $value) === false) { + unset($collection->$key); + } + } + + return $collection; + +}; + +// greater than +collection::$filters['>'] = function($collection, $field, $value) { + + foreach($collection->data as $key => $item) { + if(collection::extractValue($item, $field) > $value) continue; + unset($collection->$key); + } + + return $collection; + +}; + +// greater and equals +collection::$filters['>='] = function($collection, $field, $value) { + + foreach($collection->data as $key => $item) { + if(collection::extractValue($item, $field) >= $value) continue; + unset($collection->$key); + } + + return $collection; + +}; + +// less than +collection::$filters['<'] = function($collection, $field, $value) { + + foreach($collection->data as $key => $item) { + if(collection::extractValue($item, $field) < $value) continue; + unset($collection->$key); + } + + return $collection; + +}; + +// less and equals +collection::$filters['<='] = function($collection, $field, $value) { + + foreach($collection->data as $key => $item) { + if(collection::extractValue($item, $field) <= $value) continue; + unset($collection->$key); + } + + return $collection; + +}; diff --git a/kirby/toolkit/lib/cookie.php b/kirby/toolkit/lib/cookie.php new file mode 100644 index 0000000..f3b685a --- /dev/null +++ b/kirby/toolkit/lib/cookie.php @@ -0,0 +1,168 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Cookie { + + // configuration + public static $salt = 'KirbyToolkitCookieSalt'; + + /** + * Set a new cookie + * + * + * + * cookie::set('mycookie', 'hello', 60); + * // expires in 1 hour + * + * + * + * @param string $key The name of the cookie + * @param string $value The cookie content + * @param int $lifetime The number of minutes until the cookie expires + * @param string $path The path on the server to set the cookie for + * @param string $domain the domain + * @param boolean $secure only sets the cookie over https + * @param boolean $httpOnly avoids the cookie to be accessed via javascript + * @return boolean true: the cookie has been created, false: cookie creation failed + */ + public static function set($key, $value, $lifetime = 0, $path = '/', $domain = null, $secure = false, $httpOnly = true) { + + // convert array values to json + if(is_array($value)) $value = a::json($value); + + // hash the value + $value = static::hash($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 + * + * @return int + */ + public static function lifetime($minutes) { + return $minutes > 0 ? (time() + ($minutes * 60)) : 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 string $path The path on the server to set the cookie for + * @param string $domain the domain + * @param boolean $secure only sets the cookie over https + * @return boolean true: the cookie has been created, false: cookie creation failed + */ + public static function forever($key, $value, $path = '/', $domain = null, $secure = false) { + return static::set($key, $value, 2628000, $path, $domain, $secure); + } + + /** + * Get a cookie value + * + * + * + * cookie::get('mycookie', 'peter'); + * // sample output: 'hello' or if the cookie is not set 'peter' + * + * + * + * @param string $key The name of the cookie + * @param string $default The default value, which should be returned if the cookie has not been found + * @return mixed The found value + */ + public static function get($key = null, $default = null) { + if(is_null($key)) return $_COOKIE; + $value = isset($_COOKIE[$key]) ? $_COOKIE[$key] : null; + return empty($value) ? $default : static::parse($value); + } + + /** + * Checks if a cookie exists + * + * @return boolean + */ + public static function exists($key) { + return !is_null(static::get($key)); + } + + /** + * Creates a hash for the cookie value + * salted with the secret cookie salt string from the defaults + * + * @param string $value + * @return string + */ + protected static function hash($value) { + return sha1($value . static::$salt); + } + + /** + * Parses the hashed value from a cookie + * and tries to extract the value + * + * @param string $string + * @return mixed + */ + protected static function parse($string) { + + // extract hash and value + $parts = str::split($string, '+'); + $hash = a::first($parts); + $value = a::last($parts); + + // if the hash or the value is missing at all return null + if(empty($hash) || empty($value)) return null; + + // compare the extracted hash with the hashed value + if($hash !== static::hash($value)) return null; + + return $value; + + } + + /** + * Remove a cookie + * + * + * + * cookie::remove('mycookie'); + * // mycookie is now gone + * + * + * + * @param string $key The name of the cookie + * @return mixed true: the cookie has been removed, false: the cookie could not be removed + */ + public static function remove($key) { + if(isset($_COOKIE[$key])) { + unset($_COOKIE[$key]); + return setcookie($key, '', time() - 3600, '/'); + } + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/crypt.php b/kirby/toolkit/lib/crypt.php new file mode 100644 index 0000000..9399f7f --- /dev/null +++ b/kirby/toolkit/lib/crypt.php @@ -0,0 +1,86 @@ +, Arno Richter + * @link http://getkirby.com + * @copyright Bastian Allgeier, Arno Richter + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Crypt { + + // encryption salt - should be changed + public static $salt = '-'; + + // all available encryption modes + public static $encryption = array( + 'rijndael-128', + 'rijndael-256', + 'blowfish', + 'twofish', + 'des' + ); + + /** + * Encodes a string + * + * @param string $text + * @param string $key An optional encryption key + * @param string $mode Check out the $encryption array for available modes + * @return string + */ + public static function encode($text, $key = null, $mode = 'blowfish') { + + // check for mcrypt support + if(!function_exists('mcrypt_get_iv_size')) { + throw new Exception('The mcrypt extension is missing'); + } + + // all modes are lowercase so we try to avoid errors here + $mode = strtolower($mode); + + // check for a valid encryption mode + if(!in_array($mode, static::$encryption)) throw new Exception('Invalid encryption mode: ' . $mode); + + $size = mcrypt_get_iv_size($mode, MCRYPT_MODE_ECB); + $iv = mcrypt_create_iv($size, MCRYPT_RAND); + $result = mcrypt_encrypt($mode, static::$salt . $key, $text, MCRYPT_MODE_ECB, $iv); + + return trim($result); + + } + + /** + * Decodes a string + * + * @param string $text + * @param string $key An optional encryption key + * @param string $mode Check out the $encryption array for available modes + * @return string + */ + public static function decode($text, $key = null, $mode = 'blowfish') { + + // check for mcrypt support + if(!function_exists('mcrypt_get_iv_size')) { + throw new Exception('The mcrypt extension is missing'); + } + + // all modes are lowercase so we try to avoid errors here + $mode = strtolower($mode); + + // check for a valid encryption mode + if(!in_array($mode, static::$encryption)) throw new Exception('Invalid encryption mode: ' . $mode); + + $size = mcrypt_get_iv_size($mode, MCRYPT_MODE_ECB); + $iv = mcrypt_create_iv($size, MCRYPT_RAND); + $result = mcrypt_decrypt($mode, static::$salt . $key, $text, MCRYPT_MODE_ECB, $iv); + + return trim($result); + + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/data.php b/kirby/toolkit/lib/data.php new file mode 100644 index 0000000..4cab0f0 --- /dev/null +++ b/kirby/toolkit/lib/data.php @@ -0,0 +1,175 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Data { + + const ERROR_INVALID_ADAPTER = 0; + + public static $adapters = array(); + + public static function adapter($type) { + + if(isset(static::$adapters[$type])) return static::$adapters[$type]; + + foreach(static::$adapters as $adapter) { + if(is_array($adapter['extension']) && in_array($type, $adapter['extension'])) { + return $adapter; + } else if($adapter['extension'] == $type) { + return $adapter; + } + } + + throw new Error('Invalid adapter type', static::ERROR_INVALID_ADAPTER); + + } + + public static function encode($data, $type) { + $adapter = static::adapter($type); + return call_user_func($adapter['encode'], $data); + } + + public static function decode($data, $type) { + $adapter = static::adapter($type); + return call_user_func($adapter['decode'], $data); + } + + public static function read($file, $type = null) { + + // type autodetection + if(is_null($type)) $type = f::extension($file); + + // get the adapter + $adapter = static::adapter($type); + + if(isset($adapter['read'])) { + return call($adapter['read'], $file); + } else { + return data::decode(f::read($file), $type); + } + + + } + + public static function write($file, $data, $type = null) { + // type autodetection + if(is_null($type)) $type = f::extension($file); + return f::write($file, data::encode($data, $type)); + } + +} + + +/** + * Json adapter + */ +data::$adapters['json'] = array( + 'extension' => 'json', + 'encode' => function($data) { + return json_encode($data); + }, + 'decode' => function($string) { + return json_decode($string, true); + } +); + + +/** + * Kirby data adapter + */ +data::$adapters['kd'] = array( + 'extension' => array('md', 'txt'), + 'encode' => function($data) { + + $result = array(); + foreach($data AS $key => $value) { + $key = str::ucfirst(str::slug($key)); + + if(empty($key) || is_null($value)) continue; + + // avoid problems with arrays + if(is_array($value)) { + $value = ''; + } + + // escape accidental dividers within a field + $value = preg_replace('!(\n|^)----(.*?\R*)!', "$1\\----$2", $value); + + // multi-line content + if(preg_match('!\R!', $value, $matches)) { + $result[$key] = $key . ": \n\n" . trim($value); + // single-line content + } else { + $result[$key] = $key . ': ' . trim($value); + } + + } + return implode("\n\n----\n\n", $result); + + }, + 'decode' => function($string) { + + // remove BOM + $string = str_replace(BOM, '', $string); + // explode all fields by the line separator + $fields = preg_split('!\n----\s*\n*!', $string); + // start the data array + $data = array(); + + // loop through all fields and add them to the content + foreach($fields as $field) { + $pos = strpos($field, ':'); + $key = str_replace(array('-', ' '), '_', strtolower(trim(substr($field, 0, $pos)))); + + // Don't add fields with empty keys + if(empty($key)) continue; + $data[$key] = trim(substr($field, $pos+1)); + + } + + return $data; + + } +); + + +/** + * PHP serializer adapter + */ +data::$adapters['php'] = array( + 'extension' => array('php'), + 'encode' => function($array) { + return ''; + }, + 'decode' => function() { + throw new Error('Decoding PHP strings is not supported'); + }, + 'read' => function($file) { + $array = require $file; + return $array; + } +); + + +/** + * YAML adapter + */ +data::$adapters['yaml'] = array( + 'extension' => array('yaml', 'yml'), + 'encode' => function($data) { + return yaml::encode($data); + }, + 'decode' => function($string) { + return yaml::decode($string); + } +); \ No newline at end of file diff --git a/kirby/toolkit/lib/database.php b/kirby/toolkit/lib/database.php new file mode 100644 index 0000000..2570b1e --- /dev/null +++ b/kirby/toolkit/lib/database.php @@ -0,0 +1,496 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Database { + + public static $connectors = array(); + + // a global array of started connections + public static $connections = array(); + + // the established connection + protected $connection; + + // dsn + protected $dsn; + + // the database type (mysql, sqlite) + protected $type; + + // the connection id + protected $id; + + // the optional prefix for table names + protected $prefix; + + // the PDO query statement + protected $statement; + + // whitelists for tables and their columns + protected $tableWhitelist; + protected $columnWhitelist = array(); + + // the number of affected rows for the last query + protected $affected; + + // the last insert id + protected $lastId; + + // the last query + protected $lastQuery; + + // the last result set + protected $lastResult; + + // the last error + protected $lastError; + + // set to true to throw exceptions on failed queries + protected $fail = false; + + // an array with all queries which are being made + protected $trace = array(); + + /** + * Constructor + */ + public function __construct($params = null) { + $this->connect($params); + } + + /** + * Returns one of the started instance + * + * @param string $id + * @return object + */ + public static function instance($id = null) { + return (is_null($id)) ? a::last(static::$connections) : a::get(static::$connections, $id); + } + + /** + * Returns all started instances + * + * @return array + */ + public static function instances() { + return static::$connections; + } + + /** + * Connects to a database + * + * @param mixed $params This can either be a config key or an array of parameters for the connection + * @return object + */ + public function connect($params = null) { + + $defaults = array( + '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::$connectors[$this->type])) { + throw new Exception('Invalid database connector: ' . $this->type); + } + + // fetch the dsn and store it + $this->dsn = call_user_func(static::$connectors[$this->type], $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 object + */ + public function connection() { + return $this->connection; + } + + /** + * Sets the exception mode for the next query + * + * @param boolean $fail + */ + public function fail($fail = true) { + $this->fail = $fail; + return $this; + } + + /** + * Returns the used database type + * + * @return string + */ + public function type() { + return $this->type; + } + + /** + * Returns the used table name prefix + * + * @return string + */ + public function prefix() { + 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($value) { + 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) { + if(is_null($data)) return $this->trace; + $this->trace[] = $data; + } + + /** + * Returns the number of affected rows for the last query + * + * @return int + */ + public function affected() { + return $this->affected; + } + + /** + * Returns the last id if available + * + * @return int + */ + public function lastId() { + return $this->lastId; + } + + /** + * Returns the last query + * + * @return string + */ + public function lastQuery() { + return $this->lastQuery; + } + + /** + * Returns the last set of results + * + * @return mixed + */ + public function lastResult() { + return $this->lastResult; + } + + /** + * Returns the last db error (exception object) + * + * @return object + */ + 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 mixed + */ + protected function hit($query, $bindings = array()) { + + // 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(\Exception $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) throw $e; + + } + + // add a new entry to the singleton trace array + $this->trace(array( + 'query' => $this->lastQuery, + 'bindings' => $bindings, + 'error' => $this->lastError + )); + + // reset some stuff + $this->fail = false; + + // return true or false on success or failure + return is_null($this->lastError); + + } + + /** + * 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($query, $bindings = array(), $params = array()) { + + $defaults = array( + 'flag' => null, + 'method' => 'fetchAll', + 'fetch' => 'Obj', + 'iterator' => 'Collection', + ); + + $options = array_merge($defaults, $params); + + if(!$this->hit($query, $bindings)) 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'])) $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($query, $bindings = array()) { + return $this->lastResult = $this->hit($query, $bindings); + } + + /** + * Sets the current table, which should be queried + * + * @param string $table + * @return object Returns a Query object, which can be used to build a full query for that table + */ + public function table($table) { + return new Database\Query($this, $this->prefix() . $table); + } + + /** + * Checks if a table exists in the current database + * + * @param string $table + * @return boolean + */ + public function validateTable($table) { + if(!$this->tableWhitelist) { + // Get the table whitelist from the database + $sql = new SQL($this); + $query = $sql->tableList($this->database); + $results = $this->query($query, $sql->bindings($query)); + + if($results) { + $this->tableWhitelist = $results->pluck('name'); + } else { + return false; + } + } + + return in_array($table, $this->tableWhitelist); + } + + /** + * Checks if a column exists in a specified table + * + * @param string $table + * @param string $column + * @return boolean + */ + public function validateColumn($table, $column) { + if(!isset($this->columnWhitelist[$table])) { + if(!$this->validateTable($table)) { + $this->columnWhitelist[$table] = array(); + return false; + } + + // Get the column whitelist from the database + $sql = new SQL($this); + $query = $sql->columnList($this->database, $table); + $results = $this->query($query, $sql->bindings($query)); + + if($results) { + $this->columnWhitelist[$table] = $results->pluck('name'); + } else { + return false; + } + } + + return in_array($column, $this->columnWhitelist[$table]); + } + + /** + * Creates a new table + * + * @param string $table + * @param array $columns + * @return boolean + */ + public function createTable($table, $columns = array()) { + $sql = new SQL($this); + $query = $sql->createTable($table, $columns); + $queries = str::split($query, ';'); + + foreach($queries as $query) { + $query = trim($query); + + if(!$this->execute($query, $sql->bindings($query))) return false; + } + + return true; + + } + + /** + * Drops a table + * + * @param string $table + * @return boolean + */ + public function dropTable($table) { + $sql = new SQL($this); + $query = $sql->dropTable($table); + return $this->execute($query, $sql->bindings($query)); + } + + /** + * 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::$connectors['mysql'] = function($params) { + + if(!isset($params['host']) && !isset($params['socket'])) { + throw new Error('The mysql connection requires either a "host" or a "socket" parameter'); + } + + if(!isset($params['database'])) { + throw new Error('The mysql connection requires a "database" parameter'); + } + + $parts = array(); + + if(!empty($params['host'])) { + $parts[] = 'host=' . $params['host']; + } + + if(!empty($params['port'])) { + $parts[] = 'port=' . $params['port']; + } + + if(!empty($params['socket'])) { + $parts[] = 'unix_socket=' . $params['socket']; + } + + if(!empty($params['database'])) { + $parts[] = 'dbname=' . $params['database']; + } + + $parts[] = 'charset=' . a::get($params, 'charset', 'utf8'); + + return 'mysql:' . implode(';', $parts); + +}; + + +/** + * SQLite database connector + */ +database::$connectors['sqlite'] = function($params) { + if(!isset($params['database'])) throw new Error('The sqlite connection requires a "database" parameter'); + return 'sqlite:' . $params['database']; +}; diff --git a/kirby/toolkit/lib/database/query.php b/kirby/toolkit/lib/database/query.php new file mode 100644 index 0000000..8e6d68d --- /dev/null +++ b/kirby/toolkit/lib/database/query.php @@ -0,0 +1,923 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Query { + + const ERROR_INVALID_QUERY_METHOD = 0; + + protected $database = null; + + // The object which should be fetched for each row + protected $fetch = 'Obj'; + + // The iterator class, which should be used for result sets + protected $iterator = 'Collection'; + + // An array of bindings for the final query + protected $bindings = array(); + + // The table name + protected $table; + + // The name of the primary key column + protected $primaryKeyName = 'id'; + + // An array with additional join parameters + protected $join; + + // A list of columns, which should be selected + protected $select; + + // Boolean for distinct select clauses + protected $distinct; + + // Boolean for if exceptions should be thrown on failing queries + protected $fail = false; + + // A list of values for update and insert clauses + protected $values; + + // WHERE clause + protected $where; + + // GROUP BY clause + protected $group; + + // HAVING clause + protected $having; + + // ORDER BY clause + protected $order; + + // The offset, which should be applied to the select query + protected $offset = 0; + + // The limit, which should be applied to the select query + protected $limit; + + // Boolean to enable query debugging + protected $debug = false; + + /** + * Constructor + * + * @param Database $database Database object + * @param string $table Optional name of the table, which should be queried + */ + public function __construct($database, $table) { + $this->database = $database; + + $this->table($table); + if(!$this->table) throw new Error('Invalid table ' . $table); + } + + /** + * Reset the query class after each db hit + */ + protected function reset() { + $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 = null; + $this->limit = null; + $this->debug = null; + } + + /** + * 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 object + */ + public function debug($debug = true) { + $this->debug = $debug; + return $this; + } + + /** + * Enables distinct select clauses. + * + * @param boolean $distinct + * @return object + */ + public function distinct($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 object + */ + public function fail($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 object + */ + public function fetch($fetch) { + if(!is_null($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 object + */ + public function iterator($iterator) { + if(!is_null($iterator)) $this->iterator = $iterator; + return $this; + } + + /** + * Sets the name of the table, which should be queried + * + * @param string $table + * @return object + */ + public function table($table) { + if(!is_null($table) && $this->database->validateTable($table)) $this->table = $table; + return $this; + } + + /** + * Sets the name of the primary key column + * + * @param string $primaryKeyName + * @return object + */ + public function primaryKeyName($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 object + */ + 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($table, $on, $type = '') { + + $join = array( + '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 object + */ + public function leftJoin($table, $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 object + */ + public function rightJoin($table, $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 object + */ + 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 object + */ + public function values($values = array()) { + if(!is_null($values)) $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 mixed + */ + public function bindings($bindings = null) { + + if(is_array($bindings)) { + $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(array('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 object + */ + public function where() { + $this->where = $this->filterQuery(func_get_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 object + */ + public function orWhere() { + + $args = func_get_args(); + $mode = a::last($args); + + // if there's a where clause mode attribute attached… + if(in_array($mode, array('AND', 'OR'))) { + // remove that from the list of arguments + array_pop($args); + } + + // make sure to always attach the OR mode indicator + $args[] = 'OR'; + + call_user_func_array(array($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 object + */ + public function andWhere() { + + $args = func_get_args(); + $mode = a::last($args); + + // if there's a where clause mode attribute attached… + if(in_array($mode, array('AND', 'OR'))) { + // remove that from the list of arguments + array_pop($args); + } + + // make sure to always attach the AND mode indicator + $args[] = 'AND'; + + call_user_func_array(array($this, 'where'), func_get_args()); + return $this; + } + + /** + * Attaches a group by clause + * + * @param string $group + * @return object + */ + public function group($group) { + $this->group = $group; + return $this; + } + + /** + * Attaches an additional having clause + * + * All available ways to add having clauses + * + * ->having('username like "myuser"'); (args: 1) + * ->having(array('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 object + */ + public function having() { + $this->having = $this->filterQuery(func_get_args(), $this->having); + return $this; + } + + /** + * Attaches an order clause + * + * @param string $order + * @return object + */ + public function order($order) { + $this->order = $order; + return $this; + } + + /** + * Sets the offset for select clauses + * + * @param int $offset + * @return object + */ + public function offset($offset) { + $this->offset = $offset; + return $this; + } + + /** + * Sets the limit for select clauses + * + * @param int $limit + * @return object + */ + public function limit($limit) { + $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 = new SQL($this->database, $this); + + switch($type) { + case 'select': + + return $sql->select(array( + '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 + )); + + case 'update': + + return $sql->update(array( + 'table' => $this->table, + 'where' => $this->where, + 'values' => $this->values, + )); + + case 'insert': + + return $sql->insert(array( + 'table' => $this->table, + 'values' => $this->values, + )); + + case 'delete': + + return $sql->delete(array( + 'table' => $this->table, + 'where' => $this->where, + )); + + } + + } + + /** + * Builds a count query + * + * @return object + */ + public function count() { + return $this->aggregate('COUNT'); + } + + /** + * Builds a max query + * + * @param string $column + * @return object + */ + public function max($column) { + return $this->aggregate('MAX', $column); + } + + /** + * Builds a min query + * + * @param string $column + * @return object + */ + public function min($column) { + return $this->aggregate('MIN', $column); + } + + /** + * Builds a sum query + * + * @param string $column + * @return object + */ + public function sum($column) { + return $this->aggregate('SUM', $column); + } + + /** + * Builds an average query + * + * @param string $column + * @return object + */ + public function avg($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 object + */ + public function aggregate($method, $column = '*', $default = 0) { + + // reset the sorting to avoid counting issues + $this->order = null; + + // validate column + if($column !== '*') { + $sql = new SQL($this->database, $this); + list($table, $columnPart) = $sql->splitIdentifier($this->table, $column); + if(!$this->database->validateColumn($table, $columnPart)) { + throw new Error('Invalid column ' . $column); + } + + $column = $sql->combineIdentifier($table, $columnPart); + } + + $fetch = $this->fetch; + $row = $this->select($method . '(' . $column . ') as aggregation')->fetch('Obj')->first(); + $result = $row ? $row->get('aggregation') : $default; + $this->fetch($fetch); + return $result; + } + + /** + * Used as an internal shortcut for firing a db query + * + * @param string $query + * @param array $params + * @return mixed + */ + protected function query($query, $params = array()) { + + if($this->debug) return array( + 'query' => $query, + 'bindings' => $this->bindings(), + 'options' => $params + ); + + if($this->fail) $this->database->fail(); + + $result = $this->database->query($query, $this->bindings(), $params); + $this->reset(); + return $result; + + } + + /** + * Used as an internal shortcut for executing a db query + * + * @param string $query + * @param array $params + * @return mixed + */ + protected function execute($query, $params = array()) { + + if($this->debug) return array( + 'query' => $query, + 'bindings' => $this->bindings(), + 'options' => $params + ); + + if($this->fail) $this->database->fail(); + + $result = $this->database->execute($query, $this->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'), array( + '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 + * @param array $params Optional params for the pagination object + * @return object Collection iterator with attached pagination object + */ + public function page($page, $limit, $params = array()) { + + $defaults = array( + 'page' => $page + ); + + $options = array_merge($defaults, $params); + + // clone this to create a counter query + $counter = clone $this; + + // count the total number of rows for this query + $count = $counter->count(); + + // pagination + $pagination = new Pagination($count, $limit, $options); + + // 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 + $collection = $this->offset($pagination->offset())->limit($pagination->limit())->all(); + + // 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'), array( + 'fetch' => $this->fetch, + 'iterator' => $this->iterator, + )); + } + + /** + * Returns only values from a single column + * + * @param string $column + * @return mixed + */ + public function column($column) { + + $sql = new SQL($this->database, $this); + $primaryKey = $sql->combineIdentifier($this->table, $this->primaryKeyName); + + $results = $this->query($this->select(array($column))->order($primaryKey . ' ASC')->build('select'), array( + 'iterator' => 'array', + 'fetch' => 'array', + )); + + $results = a::extract($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(array($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')); + 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($method, $arguments) { + + if(preg_match('!^findBy([a-z]+)!i', $method, $match)) { + $column = str::lower($match[1]); + return $this->findBy($column, $arguments[0]); + } else { + throw new Error('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, array('AND', 'OR'))) { + // remove that from the list of arguments + array_pop($args); + } else { + $mode = 'AND'; + } + + switch(count($args)) { + case 1: + + if(is_null($args[0])) { + + return $current; + + // ->where('username like "myuser"'); + } else if(is_string($args[0])) { + + // 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(array('username' => 'myuser')); + } else if(is_array($args[0])) { + + $sql = new SQL($this->database, $this); + + // simple array mode (AND operator) + $result = $sql->values($this->table, $args[0], ' AND ', true, true); + + } else if(is_callable($args[0])) { + + $query = clone $this; + call_user_func($args[0], $query); + $result = '(' . $query->where . ')'; + + } + + break; + case 2: + + // ->where('username like :username', array('username' => 'myuser')) + if(is_string($args[0]) && is_array($args[1])) { + + // prepared where clause + $result = $args[0]; + + // store the bindings + $this->bindings($args[1]); + + // ->where('username like ?', 'myuser') + } else if(is_string($args[0]) && is_string($args[1])) { + + // prepared where clause + $result = $args[0]; + + // store the bindings + $this->bindings(array($args[1])); + + } + + break; + case 3: + + // ->where('username', 'like', 'myuser'); + if(is_string($args[0]) && is_string($args[1])) { + + // validate column + $sql = new SQL($this->database, $this); + list($table, $column) = $sql->splitIdentifier($this->table, $args[0]); + if(!$this->database->validateColumn($table, $column)) { + throw new Error('Invalid column ' . $args[0]); + } + $key = $sql->combineIdentifier($table, $column); + + // ->where('username', 'in', array('myuser', 'myotheruser')); + if(is_array($args[2])) { + + $predicate = trim(strtoupper($args[1])); + if(!in_array($predicate, array( + 'IN', 'NOT IN' + ))) throw new Error('Invalid predicate ' . $predicate); + + // build a list of bound values + $values = array(); + $bindings = array(); + foreach($args[2] as $value) { + $valueBinding = sql::generateBindingName('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])); + if(!in_array($predicate, array( + '=', '>=', '>', '<=', '<', '<>', '!=', '<=>', + 'IS', 'IS NOT', + 'BETWEEN', 'NOT BETWEEN', + 'LIKE', 'NOT LIKE', + 'SOUNDS LIKE', + 'REGEXP', 'NOT REGEXP' + ))) throw new Error('Invalid predicate/operator ' . $predicate); + + $valueBinding = sql::generateBindingName('value'); + $bindings[$valueBinding] = $args[2]; + + $result = $key . ' ' . $predicate . ' ' . $valueBinding; + + $this->bindings($bindings); + + } + + } + + break; + + } + + // attach the where clause + if(!empty($current)) { + return $current . ' ' . $mode . ' ' . $result; + } else { + return $result; + } + + } + +} diff --git a/kirby/toolkit/lib/db.php b/kirby/toolkit/lib/db.php new file mode 100644 index 0000000..32c7f22 --- /dev/null +++ b/kirby/toolkit/lib/db.php @@ -0,0 +1,251 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class DB { + + const ERROR_UNKNOWN_METHOD = 0; + + // query shortcuts + public static $queries = array(); + + // The singleton Database object + public static $connection = null; + + /** + * (Re)connect the database + * + * @param mixed $params Pass array() to use the default params from the config + * @return object + */ + public static function connect($params = null) { + if(is_null($params) && !is_null(static::$connection)) return static::$connection; + if(is_null($params)) { + + // try to connect with the default connection settings + $params = array( + 'type' => c::get('db.type', 'mysql'), + 'host' => c::get('db.host', 'localhost'), + 'user' => c::get('db.user', 'root'), + 'password' => c::get('db.password', ''), + 'database' => c::get('db.name', ''), + 'prefix' => c::get('db.prefix', ''), + ); + + } + + return static::$connection = new Database($params); + } + + /** + * Returns the current database connection + * + * @return object + */ + public static function connection() { + return static::$connection; + } + + /** + * Sets the current table, which should be queried + * + * @param string $table + * @return object Returns a DBQuery object, which can be used to build a full query for that table + */ + public static function table($table) { + $connection = db::connect(); + return $connection->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($query, $bindings = array(), $params = array()) { + $connection = db::connect(); + return $connection->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($query, $bindings = array()) { + $connection = db::connect(); + return $connection->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 call(static::$queries[$method], $arguments); + } else if(!is_callable(array(static::$connection, $method))) { + throw new Error('invalid static db method: ' . $method, static::ERROR_UNKNOWN_METHOD); + } else { + return call(array(static::$connection, $method), $arguments); + } + } + +} + +/** + * 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 mixed $order + * @param int $offset + * @param int $limit + * @return mixed + */ +db::$queries['select'] = function($table, $columns = '*', $where = null, $order = null, $offset = 0, $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 mixed $order + * @param int $offset + * @param int $limit + * @return mixed + */ +db::$queries['first'] = db::$queries['row'] = db::$queries['one'] = function($table, $columns = '*', $where = null, $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 mixed $column The name of the column to select from + * @param mixed $where The where clause. Can be a string or an array + * @param mixed $order + * @param int $offset + * @param int $limit + * @return mixed + */ +db::$queries['column'] = function($table, $column, $where = null, $order = null, $offset = 0, $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 string $values An array of values, which should be inserted + * @return boolean + */ +db::$queries['insert'] = function($table, $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 string $values An array of values, which should be inserted + * @param mixed $where An optional where clause + * @return boolean + */ +db::$queries['update'] = function($table, $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($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 string $where An optional where clause + * @return int + */ +db::$queries['count'] = function($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 string $where An optional where clause + * @return mixed + */ +db::$queries['min'] = function($table, $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 string $where An optional where clause + * @return mixed + */ +db::$queries['max'] = function($table, $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 string $where An optional where clause + * @return mixed + */ +db::$queries['avg'] = function($table, $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 string $where An optional where clause + * @return mixed + */ +db::$queries['sum'] = function($table, $column, $where = null) { + return db::table($table)->where($where)->sum($column); +}; \ No newline at end of file diff --git a/kirby/toolkit/lib/detect.php b/kirby/toolkit/lib/detect.php new file mode 100644 index 0000000..a436568 --- /dev/null +++ b/kirby/toolkit/lib/detect.php @@ -0,0 +1,261 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Detect { + + /** + * Checks if the mb string extension is installed + * + * @return boolean + */ + public static function mbstring() { + return function_exists('mb_split'); + } + + /** + * Checks if the required php version is installed + * + * @param mixed $min + * @return boolean + */ + public static function php($min = '5.3') { + return version_compare(PHP_VERSION, $min, '>='); + } + + /** + * Checks if PHP is running on Apache + * + * @return boolean + */ + public static function apache() { + return apache_get_version() ? true : false; + } + + /** + * Checks if the site is running on Windows + * + * @return boolean + */ + public static function windows() { + return DS == '/' ? false : true; + } + + /** + * Checks if the site is running on IIS + * + * @return boolean + */ + public static function iis() { + return isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'],'IIS') !== false ? true : false; + } + + /** + * Checks if mysql installed with the minimum required version + * + * @param mixed $min + * @return boolean + */ + public static function mysql($min = '5') { + $extensions = get_loaded_extensions(); + if(!in_array('mysql', $extensions)) return false; + $version = preg_replace('#(^\D*)([0-9.]+).*$#', '\2', mysql_get_client_info()); + return version_compare($version, $min, '>='); + } + + /** + * Checks if SQLite 3 is installed + * + * @return boolean + */ + public static function sqlite() { + return in_array('sqlite3', get_loaded_extensions()); + } + + /** + * Checks if safe mode is enabled + * + * @return boolean + */ + public static function safemode() { + return ini_get('safe_mode'); + } + + /** + * Checks if gdlib is installed + * + * @return boolean + */ + public static function gdlib() { + return function_exists('gd_info'); + } + + /** + * Checks if imageick is installed + * + * @return boolean + */ + public static function imagick() { + return class_exists('Imagick'); + } + + /** + * Checks if CURL is installed + * + * @return boolean + */ + public static function curl() { + return in_array('curl', get_loaded_extensions()); + } + + /** + * Check if APC cache is installed + * + * @return boolean + */ + public static function apc() { + return function_exists('apc_add'); + } + + /** + * Check if the Memcache extension is installed + * + * @return boolean + */ + public static function memcache() { + return class_exists('Memcache'); + } + + /** + * Check if the Memcached extension is installed + * + * @return boolean + */ + public static function memcached() { + return class_exists('Memcached'); + } + + /** + * Check if the imap extension is installed + * + * @return boolean + */ + public static function imap() { + return function_exists('imap_body'); + } + + /** + * Check if the mcrypt extension is installed + * + * @return boolean + */ + public static function mcrypt() { + return function_exists('mcrypt_encrypt'); + } + + /** + * Check if the exif extension is installed + * + * @return boolean + */ + public static function exif() { + return function_exists('read_exif_data'); + } + + /** + * Detect if the script is installed in a subfolder + * + * @return string + */ + public static function subfolder() { + return trim(dirname($_SERVER['SCRIPT_NAME']), '/\\'); + } + + /** + * Detects the current path + * + * @return string + */ + public static function path() { + $uri = explode('/', url::path()); + $script = explode('/', trim($_SERVER['SCRIPT_NAME'], '/\\')); + $parts = array_diff_assoc($uri, $script); + if(empty($parts)) return false; + return implode('/', $parts); + } + + /** + * Detect the document root + * + * @return string + */ + public static function documentRoot() { + $local = $_SERVER['SCRIPT_NAME']; + $absolute = $_SERVER['SCRIPT_FILENAME']; + return substr($absolute, 0, strpos($absolute, $local)); + } + + /** + * Converts any ini size value to an integer + * + * @param string $key + * @return int + */ + public static function iniSize($key) { + + $size = ini_get($key); + $size = trim($size); + $last = strtolower($size[strlen($size)-1]); + switch($last) { + case 'g': + $size *= 1024; + case 'm': + $size *= 1024; + case 'k': + $size *= 1024; + } + return $size; + + } + + /** + * Returns the max accepted upload size + * defined in the php.ini + * + * @return int + */ + public static function maxUploadSize() { + return static::iniSize('upload_max_filesize'); + } + + /** + * Returns the max accepted post size + * defined in the php.ini + * + * @return int + */ + public static function maxPostSize() { + return static::iniSize('post_max_size'); + } + + /** + * Dirty browser sniffing for an ios device + * + * @return boolean + */ + public static function ios() { + $ua = visitor::ua(); + return (str::contains($ua, 'iPod') || str::contains($ua, 'iPhone') || str::contains($ua, 'iPad')); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/dimensions.php b/kirby/toolkit/lib/dimensions.php new file mode 100644 index 0000000..4d52ecf --- /dev/null +++ b/kirby/toolkit/lib/dimensions.php @@ -0,0 +1,316 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Dimensions { + + // the width of the parent object + public $width = 0; + + // the height of the parent object + public $height = 0; + + /** + * Constructor + * + * @param int $width + * @param int $height + */ + public function __construct($width, $height) { + $this->width = $width; + $this->height = $height; + } + + /** + * Returns the width + * + * @return int + */ + public function width() { + return $this->width; + } + + /** + * Returns the height + * + * @return int + */ + public function height() { + return $this->height; + } + + /** + * Calculates and returns the ratio + * + * + * + * $dimensions = new Dimensions(1200, 768); + * echo $dimensions->ratio(); + * // output: 1.5625 + * + * + * + * @return float + */ + public function ratio() { + if($this->width && $this->height) { + return ($this->width / $this->height); + } else { + return 0; + } + } + + /** + * 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 boolean $force If true, the dimensions will be upscaled to fit the box if smaller + * @return object returns this object with recalculated dimensions + */ + public function fit($box, $force = false) { + + if($this->width == 0 || $this->height == 0) { + $this->width = $box; + $this->height = $box; + return $this; + } + + $ratio = $this->ratio(); + + if($this->width > $this->height) { + if($this->width > $box || $force === true) $this->width = $box; + $this->height = round($this->width / $ratio); + } elseif($this->height > $this->width) { + if($this->height > $box || $force === true) $this->height = $box; + $this->width = round($this->height * $ratio); + } elseif($this->width > $box) { + $this->width = $box; + $this->height = $box; + } + + 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 boolean $force If true, the dimensions will be upscaled to fit the width if smaller + * @return object returns this object with recalculated dimensions + */ + public function fitWidth($fit, $force = false) { + + if(!$fit) return $this; + + if($this->width <= $fit && !$force) return $this; + + $ratio = $this->ratio(); + + $this->width = $fit; + $this->height = round($fit / $ratio); + + 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 boolean $force If true, the dimensions will be upscaled to fit the height if smaller + * @return object returns this object with recalculated dimensions + */ + public function fitHeight($fit, $force = false) { + + if(!$fit) return $this; + + if($this->height <= $fit && !$force) return $this; + + $ratio = $this->ratio(); + + $this->width = round($fit * $ratio); + $this->height = $fit; + + return $this; + + } + + /** + * Recalculates the dimensions by the width and height + * + * @param int $width the max height + * @param int $height the max width + * @return object + */ + public function fitWidthAndHeight($width, $height, $force = false) { + + if($this->width > $this->height) { + + $this->fitWidth($width, $force); + + // do another check for the max height + if($this->height > $height) $this->fitHeight($height); + + } else { + + $this->fitHeight($height, $force); + + // do another check for the max width + if($this->width > $width) $this->fitWidth($width); + + } + + return $this; + + } + + /** + * @param int $width + * @param int $height + * @param boolean $force + * @return Dimensions + */ + public function resize($width, $height, $force = false) { + $this->fitWidthAndHeight($width, $height, $force); + return $this; + } + + /** + * Crops the dimensions by width and height + * + * @param int $width + * @param int $height + * @return object + */ + public function crop($width, $height = null) { + + $this->width = $width; + $this->height = $width; + + if($height) { + $this->height = $height; + } + + return $this; + + } + + /** + * Returns a string representation of the orientation + * + * @return string + */ + public function orientation() { + if(!$this->ratio()) return false; + if($this->portrait()) return 'portrait'; + if($this->landscape()) return 'landscape'; + if($this->square()) return 'square'; + } + + /** + * Checks if the dimensions are portrait + * + * @return boolean + */ + public function portrait() { + return $this->height > $this->width; + } + + /** + * Checks if the dimensions are landscape + * + * @return boolean + */ + public function landscape() { + return $this->width > $this->height; + } + + /** + * Checks if the dimensions are square + * + * @return boolean + */ + public function square() { + return $this->width == $this->height; + } + + /** + * Converts the dimensions object + * to a plain PHP array + * + * @return array + */ + public function toArray() { + return array( + 'width' => $this->width(), + 'height' => $this->height(), + 'ratio' => $this->ratio(), + 'orientation' => $this->orientation(), + ); + } + + /** + * Echos the dimensions as width × height + * + * @return string + */ + public function __toString() { + return $this->width . ' × ' . $this->height; + } + +} diff --git a/kirby/toolkit/lib/dir.php b/kirby/toolkit/lib/dir.php new file mode 100644 index 0000000..1d97293 --- /dev/null +++ b/kirby/toolkit/lib/dir.php @@ -0,0 +1,209 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Dir { + + public static $defaults = array( + 'permissions' => 0755, + 'ignore' => array('.', '..', '.DS_Store', '.gitignore', '.git', '.svn', '.htaccess', 'Thumb.db', '@eaDir') + ); + + /** + * Creates a new directory + * + * + * + * $create = dir::make('/app/test/new-directory'); + * + * if($create) echo 'the directory has been created'; + * + * + * + * @param string $dir The path for the new directory + * @return boolean True: the dir has been created, false: creating failed + */ + public static function make($dir, $recursive = true) { + return is_dir($dir) ? true : @mkdir($dir, static::$defaults['permissions'], $recursive); + } + + /** + * Reads all files from a directory and returns them as an array. + * It skips unwanted invisible stuff. + * + * + * + * $files = dir::read('mydirectory'); + * // returns array('file-1.txt', 'file-2.txt', 'file-3.txt', etc...); + * + * + * + * @param string $dir The path of directory + * @param array $ignore Optional array with filenames, which should be ignored + * @return mixed An array of filenames or false + */ + public static function read($dir, $ignore = array()) { + if(!is_dir($dir)) return array(); + $skip = array_merge(static::$defaults['ignore'], $ignore); + return (array)array_diff(scandir($dir),$skip); + } + + /** + * Moves a directory to a new location + * + * + * + * $move = dir::move('mydirectory', 'mynewdirectory'); + * + * if($move) echo 'the directory has been moved to mynewdirectory'; + * + * + * + * @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($old, $new) { + if(!is_dir($old)) return false; + return @rename($old, $new); + } + + /** + * Deletes a directory + * + * + * + * $remove = dir::remove('mydirectory'); + * + * if($remove) echo 'the directory has been removed'; + * + * + * + * @param string $dir The path of the directory + * @param boolean $keep If set to true, the directory will flushed but not removed. + * @return boolean True: the directory has been removed, false: removing failed + */ + public static function remove($dir, $keep = false) { + if(!is_dir($dir)) return false; + + // It's easier to handle this with the Folder class + $object = new Folder($dir); + return $object->remove($keep); + } + + /** + * Flushes a directory + * + * @param string $dir The path of the directory + * @return boolean True: the directory has been flushed, false: flushing failed + */ + public static function clean($dir) { + return static::remove($dir, true); + } + + /** + * 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($dir) { + + if(!file_exists($dir)) return false; + + // It's easier to handle this with the Folder class + $object = new Folder($dir); + return $object->size(); + + } + + /** + * 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($dir) { + return f::niceSize(static::size($dir)); + } + + /** + * Recursively check when the dir and all + * subfolders have been modified for the last time. + * + * @param string $dir The path of the directory + * @param string $format + * @return int + */ + public static function modified($dir, $format = null, $handler = 'date') { + // It's easier to handle this with the Folder class + $object = new Folder($dir); + return $object->modified($format, $handler); + } + + /** + * Checks if the directory or any subdirectory has been + * modified after the given timestamp + * + * @param string $dir + * @param int $time + * @return boolean + */ + public static function wasModifiedAfter($dir, $time) { + + if(filemtime($dir) > $time) return true; + + $content = dir::read($dir); + + foreach($content as $item) { + $subdir = $dir . DS . $item; + if(filemtime($subdir) > $time) return true; + if(is_dir($subdir) && dir::wasModifiedAfter($subdir, $time)) return true; + } + + return false; + + } + + /** + * Checks if the dir is writable + * + * @param string $dir + * @return boolean + */ + public static function writable($dir) { + return is_writable($dir); + } + + /** + * Checks if the dir is readable + * + * @param string $dir + * @return boolean + */ + public static function readable($dir) { + return is_readable($dir); + } + + /** + * Copy a file, or recursively copy a folder and its contents + * + * @param string $dir Source path + * @param string $to Destination path + */ + public static function copy($dir, $to) { + // It's easier to handle this with the Folder class + $object = new Folder($dir); + return $object->copy($to); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/email.php b/kirby/toolkit/lib/email.php new file mode 100644 index 0000000..b20cdc7 --- /dev/null +++ b/kirby/toolkit/lib/email.php @@ -0,0 +1,291 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Email extends Obj { + + const ERROR_INVALID_RECIPIENT = 0; + const ERROR_INVALID_SENDER = 1; + const ERROR_INVALID_REPLY_TO = 2; + const ERROR_INVALID_SUBJECT = 3; + const ERROR_INVALID_BODY = 4; + const ERROR_INVALID_SERVICE = 5; + const ERROR_DISABLED = 6; + + public static $defaults = array( + 'service' => 'mail', + 'options' => array(), + 'to' => null, + 'from' => null, + 'replyTo' => null, + 'subject' => null, + 'body' => null + ); + + public static $services = array(); + public static $disabled = false; + + public $error; + public $service; + public $options; + public $to; + public $from; + public $replyTo; + public $subject; + public $body; + + public function __construct($params = array()) { + $options = a::merge(static::$defaults, $params); + parent::__construct($options); + } + + public function __set($key, $value) { + $this->$key = $value; + } + + /** + * Validates the constructed email + * to make sure it can be sent at all + */ + public function validate() { + if(!v::email($this->extractAddress($this->to))) throw new Error('Invalid recipient', static::ERROR_INVALID_RECIPIENT); + if(!v::email($this->extractAddress($this->from))) throw new Error('Invalid sender', static::ERROR_INVALID_SENDER); + if(!v::email($this->extractAddress($this->replyTo))) throw new Error('Invalid reply address', static::ERROR_INVALID_REPLY_TO); + if(empty($this->subject)) throw new Error('Missing subject', static::ERROR_INVALID_SUBJECT); + if(empty($this->body)) throw new Error('Missing body', static::ERROR_INVALID_BODY); + } + + /** + * Public getter for the error exception + * + * @return Exception + */ + public function error() { + return $this->error; + } + + /** + * Extracts the email address from an address string + * + * @return string + */ + protected function extractAddress($string) { + if(v::email($string)) return $string; + preg_match('/<(.*?)>/i', $string, $array); + return (empty($array[1])) ? $string : $array[1]; + } + + /** + * Sends the constructed email + * + * @param array $params Optional way to set values for the email + * @return boolean + */ + public function send($params = null) { + + try { + + // fail silently if sending emails is disabled + if(static::$disabled) throw new Error('Sending emails is disabled', static::ERROR_DISABLED); + + // overwrite already set values + if(is_array($params) && !empty($params)) { + foreach(a::merge($this->toArray(), $params) as $key => $val) { + $this->set($key, $val); + } + } + + // reset all errors + $this->error = null; + + // default service + if(empty($this->service)) $this->service = 'mail'; + + // if there's no dedicated reply to address, use the from address + if(empty($this->replyTo)) $this->replyTo = $this->from; + + // validate the email + $this->validate(); + + // check if the email service is available + if(!isset(static::$services[$this->service])) { + throw new Error('The email service is not available: ' . $this->service, static::ERROR_INVALID_SERVICE); + } + + // run the service + call(static::$services[$this->service], $this); + + // reset the error + $this->error = null; + return true; + + } catch(Exception $e) { + $this->error = $e; + return false; + } + + } + +} + + +/** + * Default mail driver + */ +email::$services['mail'] = function($email) { + + $headers = array( + 'From: ' . $email->from, + 'Reply-To: ' . $email->replyTo, + 'Return-Path: ' . $email->replyTo, + 'Message-ID: <' . time() . '-' . $email->from . '>', + 'X-Mailer: PHP v' . phpversion(), + 'Content-Type: text/plain; charset=utf-8', + 'Content-Transfer-Encoding: 8bit', + ); + + ini_set('sendmail_from', $email->from); + $send = mail($email->to, str::utf8($email->subject), str::utf8($email->body), implode(PHP_EOL, $headers)); + ini_restore('sendmail_from'); + + if(!$send) { + throw new Error('The email could not be sent'); + } + +}; + +/** + * Amazon mail driver + */ +email::$services['amazon'] = function($email) { + + if(empty($email->options['key'])) throw new Error('Missing Amazon API key'); + if(empty($email->options['secret'])) throw new Error('Missing Amazon API secret'); + + $setup = array( + 'Action' => 'SendEmail', + 'Destination.ToAddresses.member.1' => $email->to, + 'ReplyToAddresses.member.1' => $email->replyTo, + 'ReturnPath' => $email->replyTo, + 'Source' => $email->from, + 'Message.Subject.Data' => $email->subject, + 'Message.Body.Text.Data' => $email->body + ); + + $params = array(); + + foreach($setup as $key => $value) { + $params[] = $key . '=' . str_replace('%7E', '~', rawurlencode($value)); + } + + sort($params, SORT_STRING); + + $host = a::get($email->options, 'host', 'email.us-east-1.amazonaws.com'); + $url = 'https://' . $host . '/'; + $date = gmdate('D, d M Y H:i:s e'); + $signature = base64_encode(hash_hmac('sha256', $date, $email->options['secret'], true)); + $query = implode('&', $params); + $headers = array(); + $auth = 'AWS3-HTTPS AWSAccessKeyId=' . $email->options['key']; + $auth .= ',Algorithm=HmacSHA256,Signature=' . $signature; + + $headers[] = 'Date: ' . $date; + $headers[] = 'Host: ' . $host; + $headers[] = 'X-Amzn-Authorization: '. $auth; + $headers[] = 'Content-Type: application/x-www-form-urlencoded'; + + $email->response = remote::post($url, array( + 'data' => $query, + 'headers' => $headers + )); + + if(!in_array($email->response->code(), array(200, 201, 202, 204))) { + throw new Error('The mail could not be sent!', $email->response->code()); + } + +}; + +/** + * Mailgun mail driver + */ +email::$services['mailgun'] = function($email) { + + if(empty($email->options['key'])) throw new Error('Missing Mailgun API key'); + if(empty($email->options['domain'])) throw new Error('Missing Mailgun API domain'); + + $url = 'https://api.mailgun.net/v2/' . $email->options['domain'] . '/messages'; + $auth = base64_encode('api:' . $email->options['key']); + + $headers = array( + 'Accept: application/json', + 'Authorization: Basic ' . $auth + ); + + $data = array( + 'from' => $email->from, + 'to' => $email->to, + 'subject' => $email->subject, + 'text' => $email->body, + 'h:Reply-To' => $email->replyTo, + ); + + $email->response = remote::post($url, array( + 'data' => $data, + 'headers' => $headers + )); + + if($email->response->code() != 200) { + throw new Error('The mail could not be sent!'); + } + +}; + +/** + * Postmark mail driver + */ +email::$services['postmark'] = function($email) { + + if(empty($email->options['key'])) throw new Error('Invalid Postmark API Key'); + + // reset the api key if we are in test mode + if(a::get($email->options, 'test')) $email->options['key'] = 'POSTMARK_API_TEST'; + + // the url for postmarks api + $url = 'https://api.postmarkapp.com/email'; + + $headers = array( + 'Accept: application/json', + 'Content-Type: application/json', + 'X-Postmark-Server-Token: ' . $email->options['key'] + ); + + $data = array( + 'From' => $email->from, + 'To' => $email->to, + 'ReplyTo' => $email->replyTo, + 'Subject' => $email->subject, + 'TextBody' => $email->body + ); + + // fetch the response + $email->response = remote::post($url, array( + 'data' => json_encode($data), + 'headers' => $headers + )); + + if($email->response->code() != 200) { + throw new Error('The mail could not be sent'); + } + +}; diff --git a/kirby/toolkit/lib/embed.php b/kirby/toolkit/lib/embed.php new file mode 100644 index 0000000..34a9365 --- /dev/null +++ b/kirby/toolkit/lib/embed.php @@ -0,0 +1,124 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Embed { + + /** + * Embeds a youtube video by passing the Youtube url + * + * @param string $url Youtube url i.e. http://www.youtube.com/watch?v=d9NF2edxy-M + * @param array $attr Additional attributes for the iframe + * @return string + */ + public static function youtube($url, $attr = array()) { + + // http://www.youtube.com/embed/d9NF2edxy-M + if(preg_match('!youtube.com\/embed\/([a-z0-9_-]+)!i', $url, $array)) { + $id = $array[1]; + // http://www.youtube.com/watch?feature=player_embedded&v=d9NF2edxy-M#! + } elseif(preg_match('!v=([a-z0-9_-]+)!i', $url, $array)) { + $id = $array[1]; + // http://youtu.be/d9NF2edxy-M + } elseif(preg_match('!youtu.be\/([a-z0-9_-]+)!i', $url, $array)) { + $id = $array[1]; + } + + // no id no result! + if(empty($id)) return false; + + // default options + if(!empty($attr['options'])) { + $options = '?' . http_build_query($attr['options']); + // options should not propagate to the attr list + unset($attr['options']); + } else { + $options = ''; + } + + // default attributes + $attr = array_merge(array( + 'src' => '//youtube.com/embed/' . $id . $options, + 'frameborder' => '0', + 'webkitAllowFullScreen' => 'true', + 'mozAllowFullScreen' => 'true', + 'allowFullScreen' => 'true', + 'width' => '100%', + 'height' => '100%', + ), $attr); + + return html::tag('iframe', '', $attr); + + } + + /** + * Embeds a vimeo video by passing the vimeo url + * + * @param string $url vimeo url i.e. http://vimeo.com/52345557 + * @param array $attr Additional attributes for the iframe + * @return string + */ + public static function vimeo($url, $attr = array()) { + + // get the uid from the url + if(preg_match('!vimeo.com\/([0-9]+)!i', $url, $array)) { + $id = $array[1]; + } else { + $id = null; + } + + // no id no result! + if(empty($id)) return false; + + // default options + if(!empty($attr['options'])) { + $options = '?' . http_build_query($attr['options']); + // options should not propagate to the attr list + unset($attr['options']); + } else { + $options = ''; + } + + // default attributes + $attr = array_merge(array( + 'src' => '//player.vimeo.com/video/' . $id . $options, + 'frameborder' => '0', + 'webkitAllowFullScreen' => 'true', + 'mozAllowFullScreen' => 'true', + 'allowFullScreen' => 'true', + 'width' => '100%', + 'height' => '100%', + ), $attr); + + return html::tag('iframe', '', $attr); + + } + + /** + * Embeds a github gist + * + * @param string $url Gist url: i.e. https://gist.github.com/2924148 + * @param string $file The name of a particular file from the gist, which should displayed only. + * @return string + */ + public static function gist($url, $file = null) { + + // url for the script file + $url = $url . '.js' . r(!is_null($file), '?file=' . $file); + + // load the gist + return html::tag('script', '', array('src' => $url)); + + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/error.php b/kirby/toolkit/lib/error.php new file mode 100644 index 0000000..6ded65b --- /dev/null +++ b/kirby/toolkit/lib/error.php @@ -0,0 +1,26 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Error extends Exception { + + public function message() { + return $this->message; + } + + public function code() { + return $this->code; + } + + public function __toString() { + return $this->message; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/errorreporting.php b/kirby/toolkit/lib/errorreporting.php new file mode 100644 index 0000000..118231e --- /dev/null +++ b/kirby/toolkit/lib/errorreporting.php @@ -0,0 +1,94 @@ + + * @link http://getkirby.com + * @copyright Lukas Bestle + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class ErrorReporting { + + /** + * Returns the current raw value + * + * @return int The current value + */ + public static function get() { + return error_reporting(); + } + + /** + * Sets a new raw error reporting value + * + * @param int $level The new level to set + * @return int The new value + */ + public static function set($level) { + if(static::get() !== error_reporting($level)) { + throw new Exception('Internal error: error_reporting() did not return the old value.'); + } + return static::get(); + } + + /** + * Check if the current error reporting includes an error level + * + * @param mixed $level The level to check for + * @param int $current A custom current level + * @return boolean + */ + public static function includes($level, $current = null) { + // also allow strings + if(is_string($level)) { + if(defined($level)) { + $level = constant($level); + } else if(defined('E_' . strtoupper($level))) { + $level = constant('E_' . strtoupper($level)); + } else { + throw new Exception('The level "' . $level . '" does not exist.'); + } + } + + $value = ($current)? $current : static::get(); + return bitmask::includes($level, $value); + } + + /** + * Adds a level to the current error reporting + * + * @param int $level The level to add + * @return boolean + */ + public static function add($level) { + // check if it is already added + if(static::includes($level)) return false; + + $old = static::get(); + $newExpected = bitmask::add($level, $old); + $newActual = static::set($newExpected); + + return $newActual === $newExpected; + } + + /** + * Removes a level from the current error reporting + * + * @param int $level The level to remove + * @return boolean + */ + public static function remove($level) { + // check if it is already removed + if(!static::includes($level)) return false; + + $old = static::get(); + $newExpected = bitmask::remove($level, $old); + $newActual = static::set($newExpected); + + return $newActual === $newExpected; + } +} diff --git a/kirby/toolkit/lib/escape.php b/kirby/toolkit/lib/escape.php new file mode 100644 index 0000000..db748a0 --- /dev/null +++ b/kirby/toolkit/lib/escape.php @@ -0,0 +1,266 @@ + + * @link https://github.com/ezraverheijen/escape + * @copyright Ezra Verheijen + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Escape { + + /** + * Check if a string needs to be escaped or not + * + * @param string $string + * @return boolean + */ + public static function noNeedToEscape($string) { + return $string === '' || ctype_digit($string); + } + + /** + * Convert a character from UTF-8 to UTF-16BE + * + * @param string $char + * @return string + */ + public static function convertEncoding($char) { + return str::convert($char, 'UTF-16BE', 'UTF-8'); + } + + /** + * Check if a character is undefined in HTML + * + * @param string $char + * @return boolean + */ + public static function charIsUndefined($char) { + $ascii = ord($char); + return ($ascii <= 0x1f && $char != "\t" && $char != "\n" && $char != "\r") + || ($ascii >= 0x7f && $ascii <= 0x9f); + } + + /** + * 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...
+ * + * @uses ENT_SUBSTITUE if available (PHP >= 5.4) + * + * @param string $string + * @return string + */ + public static function html($string) { + $flags = ENT_QUOTES; + if(defined('ENT_SUBSTITUTE')) { + $flags |= ENT_SUBSTITUTE; + } + return htmlspecialchars($string, $flags, 'UTF-8'); + } + + /** + * Escape XML element content + * + * Removes offending characters that could be wrongfully interpreted as XML markup. + * + * The following characters are reserved in XML and will be replaced with their + * corresponding XML entities: + * + * ' is replaced with ' + * " is replaced with " + * & is replaced with & + * < is replaced with < + * > is replaced with > + * + * @uses ENT_XML1 if available (PHP >= 5.4) + * + * @param string $string + * @return string + */ + public static function xml($string) { + if (defined('ENT_XML1')) { + return htmlspecialchars($string, ENT_QUOTES | ENT_XML1, 'UTF-8'); + } else { + return str_replace(''', ''', htmlspecialchars($string, ENT_QUOTES, 'UTF-8')); + } + } + + /** + * Escape common HTML attributes data + * + * This can be used to put untrusted data into typical attribute values + * like width, name, value, etc. + * + * This should not be used for complex attributes like href, src, style, + * or any of the event handlers like onmouseover. + * Use esc($string, 'js') for event handler attributes, esc($string, 'url') + * for src attributes and esc($string, 'css') for style attributes. + * + *
content
+ *
content
+ *
content
+ * + * @param string $string + * @param string $strict Whether to escape characters like [space] % * + , - / ; < = > ^ and | + * which is necessary in case of unquoted HTML attributes. + * @return string + */ + public static function attr($string, $strict = false) { + if(static::noNeedToEscape($string)) return $string; + if($strict !== true) { + return preg_replace_callback('/[^a-z0-9,\.\-_]/iSu', 'static::escapeAttrChar', $string); + } + return static::html($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) { + if(static::noNeedToEscape($string)) return $string; + return preg_replace_callback('/[^a-z0-9,\._]/iSu', 'static::escapeJSChar', $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) { + if(static::noNeedToEscape($string)) return $string; + return preg_replace_callback('/[^a-z0-9]/iSu', 'static::escapeCSSChar', $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 character for HTML attribute + * + * Callback function for preg_replace_callback() that applies HTML attribute + * escaping to all matches. + * + * @param array $matches + * @return mixed Unicode replacement if character is undefined in HTML, + * named HTML entity if available (only those that XML supports), + * upper hex entity if a named entity does not exist or + * entity with the &#xHH; format if ASCII value is less than 256. + */ + protected static function escapeAttrChar($matches) { + $char = $matches[0]; + + if(static::charIsUndefined($char)) { + return '�'; + } + + $dec = hexdec(bin2hex($char)); + + $namedEntities = array( + 34 => '"', // " + 38 => '&', // & + 60 => '<', // < + 62 => '>' // > + ); + + if(isset($namedEntities[$dec])) { + return $namedEntities[$dec]; + } + + if($dec > 255) { + return sprintf('&#x%04X;', $dec); + } + + return sprintf('&#x%02X;', $dec); + } + + /** + * Escape character for JavaScript + * + * Callback function for preg_replace_callback() that applies Javascript + * escaping to all matches. + * + * @param array $matches + * @return string + */ + protected static function escapeJSChar($matches) { + $char = $matches[0]; + if(str::length($char) == 1) { + return sprintf('\\x%02X', ord($char)); + } + $char = static::convertEncoding($char); + return sprintf('\\u%04s', str::upper(bin2hex($char))); + } + + /** + * Escape character for CSS + * + * Callback function for preg_replace_callback() that applies CSS + * escaping to all matches. + * + * @param array $matches + * @return string + */ + protected static function escapeCSSChar($matches) { + $char = $matches[0]; + if(str::length($char) == 1) { + $ord = ord($char); + } else { + $char = static::convertEncoding($char); + $ord = hexdec(bin2hex($char)); + } + return sprintf('\\%X ', $ord); + } + +} diff --git a/kirby/toolkit/lib/exif.php b/kirby/toolkit/lib/exif.php new file mode 100644 index 0000000..02cd717 --- /dev/null +++ b/kirby/toolkit/lib/exif.php @@ -0,0 +1,221 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Exif { + + // the parent media object + protected $media = null; + + // the raw exif array + protected $data = null; + + // the camera object with model and make + protected $camera = null; + + // the location object + protected $location = null; + + // the timestamp + protected $timestamp = null; + + // the exposure value + protected $exposure = null; + + // the aperture value + protected $aperture = null; + + // iso value + protected $iso = null; + + // focal length + protected $focalLength = null; + + // color or black/white + protected $isColor = null; + + /** + * Constructor + * + * @param Media $media + */ + public function __construct(Media $media) { + $this->media = $media; + $this->parse(); + } + + /** + * Returns the raw data array from the parser + * + * @return array + */ + public function data() { + return $this->data; + } + + /** + * Returns the Camera object + * + * @return object KirbyExifCamera + */ + public function camera() { + + if(!is_null($this->camera)) return $this->camera; + + // check for valid exif data + if(!is_array($this->data)) return null; + + // initialize and return it + return $this->camera = new Exif\Camera($this->data); + + } + + /** + * Returns the location object + * + * @return object ExifLocation + */ + public function location() { + + if(!is_null($this->location)) return $this->location; + + // check for valid exif data + if(!is_array($this->data)) return null; + + // initialize and return it + return $this->location = new Exif\Location($this->data); + + } + + /** + * Returns the timestamp + * + * @return string + */ + public function timestamp() { + return $this->timestamp; + } + + /** + * Returns the exposure + * + * @return string + */ + public function exposure() { + return $this->exposure; + } + + /** + * Returns the aperture + * + * @return string + */ + public function aperture() { + return $this->aperture; + } + + /** + * Returns the iso value + * + * @return int + */ + public function iso() { + return $this->iso; + } + + /** + * Checks if this is a color picture + * + * @return boolean + */ + public function isColor() { + return $this->isColor; + } + + /** + * Checks if this is a bw picture + * + * @return boolean + */ + public function isBW() { + return !$this->isColor; + } + + /** + * Returns the focal length + * + * @return string + */ + public function focalLength() { + return $this->focalLength; + } + + /** + * Pareses and stores all relevant exif data + */ + protected function parse() { + + // read the exif data of the media object if possible + $this->data = @read_exif_data($this->media->root()); + + // stop on invalid exif data + if(!is_array($this->data)) return false; + + // store the timestamp when the picture has been taken + if(isset($this->data['DateTimeOriginal'])) { + $this->timestamp = strtotime($this->data['DateTimeOriginal']); + } else { + $this->timestamp = a::get($this->data, 'FileDateTime', $this->media->modified()); + } + + // exposure + $this->exposure = a::get($this->data, 'ExposureTime'); + + // iso + $this->iso = a::get($this->data, 'ISOSpeedRatings'); + + // focal length + if(isset($this->data['FocalLength'])) { + $this->focalLength = $this->data['FocalLength']; + } else if(isset($this->data['FocalLengthIn35mmFilm'])) { + $this->focalLength = $this->data['FocalLengthIn35mmFilm']; + } + + // aperture + $this->aperture = @$this->data['COMPUTED']['ApertureFNumber']; + + // color or bw + $this->isColor = @$this->data['COMPUTED']['IsColor'] == true; + + } + + /** + * Converts the object into a nicely readable array + * + * @return array + */ + public function toArray() { + + return array( + 'camera' => $this->camera()->toArray(), + 'location' => $this->location()->toArray(), + 'timestamp' => $this->timestamp(), + 'exposure' => $this->exposure(), + 'aperture' => $this->aperture(), + 'iso' => $this->iso(), + 'focalLength' => $this->focalLength(), + 'isColor' => $this->isColor() + ); + + } + +} diff --git a/kirby/toolkit/lib/exif/camera.php b/kirby/toolkit/lib/exif/camera.php new file mode 100644 index 0000000..7bfeff0 --- /dev/null +++ b/kirby/toolkit/lib/exif/camera.php @@ -0,0 +1,68 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Camera { + + protected $make; + protected $model; + + /** + * Constructor + * + * @param array $exif + */ + public function __construct($exif) { + $this->make = @$exif['Make']; + $this->model = @$exif['Model']; + } + + /** + * Returns the make of the camera + * + * @return string + */ + public function make() { + return $this->make; + } + + /** + * Returns the camera model + * + * @return string + */ + public function model() { + return $this->model; + } + + /** + * Converts the object into a nicely readable array + * + * @return array + */ + public function toArray() { + return array( + 'make' => $this->make, + 'model' => $this->model + ); + } + + /** + * Returns the full make + model name + * + * @return string + */ + public function __toString() { + return trim($this->make . ' ' . $this->model); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/exif/location.php b/kirby/toolkit/lib/exif/location.php new file mode 100644 index 0000000..a62ab66 --- /dev/null +++ b/kirby/toolkit/lib/exif/location.php @@ -0,0 +1,118 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Location { + + // latitude + protected $lat; + + // longitude + protected $lng; + + /** + * Constructor + * + * @param array $exif The entire exif array + */ + public function __construct($exif) { + + if( + isset($exif['GPSLatitude']) && + isset($exif['GPSLatitudeRef']) && + isset($exif['GPSLongitude']) && + isset($exif['GPSLongitudeRef']) + ) { + $this->lat = $this->gps($exif['GPSLatitude'], $exif['GPSLatitudeRef']); + $this->lng = $this->gps($exif['GPSLongitude'], $exif['GPSLongitudeRef']); + } + + } + + /** + * Returns the latitude + * + * @return float + */ + public function lat() { + return $this->lat; + } + + /** + * Returns the longitude + * + * @return float + */ + public function lng() { + return $this->lng; + } + + /** + * Converts the gps coordinates + * + * @param string $coord + * @param string $hemi + * @return float + */ + protected function gps($coord, $hemi) { + + $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($part) { + + $parts = explode('/', $part); + + if(count($parts) <= 0) return 0; + 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() { + return array( + 'lat' => $this->lat(), + 'lng' => $this->lng() + ); + } + + /** + * Echos the entire location as lat, lng + * + * @return string + */ + public function __toString() { + return trim(trim($this->lat() . ', ' . $this->lng(), ',')); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/f.php b/kirby/toolkit/lib/f.php new file mode 100644 index 0000000..dd27d6c --- /dev/null +++ b/kirby/toolkit/lib/f.php @@ -0,0 +1,794 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class F { + + public static $mimes = array( + 'hqx' => 'application/mac-binhex40', + 'cpt' => 'application/mac-compactpro', + 'csv' => array('text/x-comma-separated-values', 'text/comma-separated-values', 'application/octet-stream'), + 'bin' => 'application/macbinary', + 'dms' => 'application/octet-stream', + 'lha' => 'application/octet-stream', + 'lzh' => 'application/octet-stream', + 'exe' => array('application/octet-stream', 'application/x-msdownload'), + 'class' => 'application/octet-stream', + 'psd' => 'application/x-photoshop', + 'so' => 'application/octet-stream', + 'sea' => 'application/octet-stream', + 'dll' => 'application/octet-stream', + 'oda' => 'application/oda', + 'pdf' => array('application/pdf', 'application/x-download'), + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + 'smi' => 'application/smil', + 'smil' => 'application/smil', + 'mif' => 'application/vnd.mif', + 'wbxml' => 'application/wbxml', + 'wmlc' => 'application/wmlc', + 'dcr' => 'application/x-director', + 'dir' => 'application/x-director', + 'dxr' => 'application/x-director', + 'dvi' => 'application/x-dvi', + 'gtar' => 'application/x-gtar', + 'gz' => 'application/x-gzip', + 'php' => array('text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'), + 'php3' => array('text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'), + 'phtml' => array('text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'), + 'phps' => array('text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'), + 'js' => 'application/x-javascript', + 'swf' => 'application/x-shockwave-flash', + 'sit' => 'application/x-stuffit', + 'tar' => 'application/x-tar', + 'tgz' => array('application/x-tar', 'application/x-gzip-compressed'), + 'xhtml' => 'application/xhtml+xml', + 'xht' => 'application/xhtml+xml', + 'zip' => array('application/x-zip', 'application/zip', 'application/x-zip-compressed'), + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mpga' => 'audio/mpeg', + 'mp2' => 'audio/mpeg', + 'mp3' => array('audio/mpeg', 'audio/mpg', 'audio/mpeg3', 'audio/mp3'), + 'aif' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'ram' => 'audio/x-pn-realaudio', + 'rm' => 'audio/x-pn-realaudio', + 'rpm' => 'audio/x-pn-realaudio-plugin', + 'ra' => 'audio/x-realaudio', + 'rv' => 'video/vnd.rn-realvideo', + 'wav' => 'audio/x-wav', + 'bmp' => 'image/bmp', + 'gif' => 'image/gif', + 'ico' => 'image/x-icon', + 'jpg' => array('image/jpeg', 'image/pjpeg'), + 'jpeg' => array('image/jpeg', 'image/pjpeg'), + 'jpe' => array('image/jpeg', 'image/pjpeg'), + 'png' => 'image/png', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'svg' => 'image/svg+xml', + 'css' => 'text/css', + 'html' => 'text/html', + 'htm' => 'text/html', + 'shtml' => 'text/html', + 'txt' => 'text/plain', + 'text' => 'text/plain', + 'log' => array('text/plain', 'text/x-log'), + 'rtx' => 'text/richtext', + 'rtf' => 'text/rtf', + 'xml' => 'text/xml', + 'xsl' => 'text/xml', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mpe' => 'video/mpeg', + 'qt' => 'video/quicktime', + 'mov' => 'video/quicktime', + 'avi' => 'video/x-msvideo', + 'movie' => 'video/x-sgi-movie', + 'doc' => 'application/msword', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'xls' => array('application/excel', 'application/vnd.ms-excel', 'application/msexcel'), + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'ppt' => array('application/powerpoint', 'application/vnd.ms-powerpoint'), + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'word' => array('application/msword', 'application/octet-stream'), + 'xl' => 'application/excel', + 'eml' => 'message/rfc822', + 'json' => array('application/json', 'text/json'), + 'odt' => 'application/vnd.oasis.opendocument.text', + 'odc' => 'application/vnd.oasis.opendocument.chart', + 'odp' => 'application/vnd.oasis.opendocument.presentation', + ); + + public static $types = array( + + 'image' => array( + 'jpeg', + 'jpg', + 'jpe', + 'gif', + 'png', + 'svg', + 'ico', + 'tif', + 'tiff', + 'bmp', + 'psd', + 'ai', + 'eps', + 'ps' + ), + + 'document' => array( + 'txt', + 'text', + 'mdown', + 'md', + 'markdown', + 'pdf', + 'doc', + 'docx', + 'dotx', + 'word', + 'xl', + 'xls', + 'xlsx', + 'xltx', + 'ppt', + 'pptx', + 'potx', + 'csv', + 'rtf', + 'rtx', + 'log', + 'odt', + 'odp', + 'odc', + ), + + 'archive' => array( + 'zip', + 'tar', + 'gz', + 'gzip', + 'tgz', + ), + + 'code' => array( + 'js', + 'css', + 'scss', + 'htm', + 'html', + 'shtml', + 'xhtml', + 'php', + 'php3', + 'php4', + 'rb', + 'xml', + 'json', + 'java', + 'py' + ), + + 'video' => array( + 'mov', + 'movie', + 'avi', + 'ogg', + 'ogv', + 'webm', + 'flv', + 'swf', + 'mp4', + 'm4v', + 'mpg', + 'mpe' + ), + + 'audio' => array( + 'mp3', + 'm4a', + 'wav', + 'aif', + 'aiff', + 'midi', + ), + + ); + + public static $units = array('B','kB','MB','GB','TB','PB', 'EB', 'ZB', 'YB'); + + /** + * Checks if a file exists + * + * @param string $file + * @return boolean + */ + public static function exists($file) { + return file_exists($file); + } + + /** + * Safely requires a file if it exists + */ + public static function load($file, $data = array()) { + if(file_exists($file)) { + extract($data); + require($file); + } + } + + /** + * Creates a new file + * + * + * + * f::write('test.txt', 'hello'); + * // creates a new text file with hello as content + * + * // create a new file + * f::write('text.txt', array('test' => 'hello')); + * // creates a new file and encodes the array as json + * + * + * + * @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($file, $content, $append = false) { + if(is_array($content) || is_object($content)) $content = serialize($content); + $mode = ($append) ? FILE_APPEND | LOCK_EX : LOCK_EX; + // if the parent directory does not exist, create it + if(!is_dir(dirname($file))) { + if(!dir::make(dirname($file))) return false; + } + return (@file_put_contents($file, $content, $mode) !== false) ? true : false; + } + + /** + * 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($file, $content) { + return static::write($file,$content,true); + } + + /** + * Reads the content of a file + * + * + * + * $content = f::read('test.txt'); + * // i.e. content is hello + * + * $content = f::read('text.txt', 'json'); + * // returns an array with the parsed content + * + * + * + * @param string $file The path for the file + * @return mixed + */ + public static function read($file) { + return @file_get_contents($file); + } + + /** + * Returns the file content as base64 encoded string + * + * @param string $file The path for the file + * @return string + */ + public static function base64($file) { + return base64_encode(f::read($file)); + } + + /** + * Returns the file as data uri + * + * @param string $file The path for the file + * @return string + */ + public static function uri($file) { + $mime = static::mime($file); + return ($mime) ? 'data:' . $mime . ';base64,' . static::base64($file) : false; + } + + /** + * Moves a file to a new location + * + * + * + * $move = f::move('test.txt', 'super.txt'); + * + * if($move) echo 'The file has been moved'; + * + * + * + * @param string $old The current path for the file + * @param string $new The path to the new location + * @return boolean + */ + public static function move($old, $new) { + if(!file_exists($old) || file_exists($new)) return false; + return @rename($old, $new); + } + + /** + * Copy a file to a new location. + * + * @param string $file + * @param string $target + * @return boolean + */ + public static function copy($file, $target) { + if(!file_exists($file) || file_exists($target)) return false; + return @copy($file, $target); + } + + /** + * 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($file) { + return file_exists($file) && is_file($file) && !empty($file) ? @unlink($file) : false; + } + + /** + * Gets the extension of a file + * + * + * + * $extension = f::extension('test.txt'); + * // extension is txt + * + * + * + * @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($file, $extension = false) { + + // overwrite the current extension + if($extension !== false) { + return static::name($file) . '.' . $extension; + } + + // return the current extension + return strtolower(pathinfo($file, PATHINFO_EXTENSION)); + + } + + /** + * Returns all extensions for a certain file type + * + * @param string $type + * @return array + */ + public static function extensions($type = null) { + if(is_null($type)) return array_keys(static::$mimes); + return isset(static::$types[$type]) ? static::$types[$type] : array(); + } + + /** + * 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($name) { + return pathinfo($name, PATHINFO_BASENAME); + } + + /** + * Extracts the name from a file path or filename without extension + * + * + * + * $name = f::name('/var/www/test.txt'); + * + * // name is test + * + * + * + * @param string $name The path or filename + * @return string + */ + public static function name($name) { + return pathinfo($name, PATHINFO_FILENAME); + } + + /** + * 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($file) { + return dirname($file); + } + + /** + * Returns the size of a file. + * + * + * + * $size = f::size('/var/www/test.txt'); + * // size is ie: 1231939 + * + * + * + * @param mixed $file The path + * @return mixed + */ + public static function size($file) { + return filesize($file); + } + + /** + * Converts an integer size into a human readable format + * + * + * + * $niceSize = f::niceSize('/path/to/a/file.txt'); + * // nice size is i.e: 212 kb + * + * $niceSize = f::niceSize(1231939); + * // nice size is: 1,2 mb + * + * + * + * @param mixed $size The file size or a file path + * @return string + */ + public static function niceSize($size) { + + // file mode + if(is_string($size) && file_exists($size)) { + $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]; + + } + + /** + * Get the file's last modification time. + * + * @param string $file + * @param string $format + * @param string $handler date or strftime + * @return int + */ + public static function modified($file, $format = null, $handler = 'date') { + if(file_exists($file)) { + return !is_null($format) ? $handler($format, filemtime($file)) : filemtime($file); + } else { + return false; + } + } + + /** + * Returns the mime type of a file + * + * @param string $file + * @return mixed + */ + public static function mime($file) { + + // stop for invalid files + if(!file_exists($file)) return null; + + // Fileinfo is prefered if available + if(function_exists('finfo_file')) { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime = finfo_file($finfo, $file); + finfo_close($finfo); + return $mime; + } + + // for older versions with mime_content_type go for that. + if(function_exists('mime_content_type') && $mime = @mime_content_type($file) !== false) { + return $mime; + } + + // shell check + try { + $mime = system::execute('file', [$file, '-z', '-b', '--mime'], 'output'); + $mime = trim(str::split($mime, ';')[0]); + if(f::mimeToExtension($mime)) return $mime; + } catch(Exception $e) { + // no mime type detectable with shell + $mime = null; + } + + // Mime Sniffing + $reader = new MimeReader($file); + $mime = $reader->get_type(); + + if(!empty($mime) && f::mimeToExtension($mime)) { + return $mime; + } + + // guess the matching mime type by extension + return f::extensionToMime(f::extension($file)); + + } + + /** + * Returns all detectable mime types + * + * @return array + */ + public static function mimes() { + return static::$mimes; + } + + /** + * Categorize the file + * + * @param string $file Either the file path or extension + * @return string + */ + public static function type($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)) { + // 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)) return $type; + } + + return null; + + } + + /** + * Returns an array of all available file types + * + * @return array + */ + public static function types() { + return static::$types; + } + + /** + * 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($file, $value) { + + if(in_array($value, static::extensions())) { + // check for the extension + return static::extension($file) == $value; + } else if(strpos($value, '/') !== false) { + // check for the mime type + return static::mime($file) == $value; + } + + return false; + + } + + /** + * Converts a mime type to a file extension + * + * @param string $mime + * @return string + */ + public static function mimeToExtension($mime) { + foreach(static::$mimes as $key => $value) { + if(is_array($value) && in_array($mime, $value)) return $key; + if($value == $mime) return $key; + } + return null; + } + + /** + * Returns the type for a given mime + * + * @param string $mime + * @return string + */ + public static function mimeToType($mime) { + return static::extensionToType(static::mimeToExtension($mime)); + } + + /** + * Converts a file extension to a mime type + * + * @param string $extension + * @return string + */ + public static function extensionToMime($extension) { + $mime = isset(static::$mimes[$extension]) ? static::$mimes[$extension] : null; + return is_array($mime) ? array_shift($mime) : $mime; + } + + /** + * Returns the file type for a passed extension + * + * @param string $extension + * @return string + */ + public static function extensionToType($extension) { + + // get all categorized types + foreach(static::$types as $type => $extensions) { + if(in_array($extension, $extensions)) return $type; + } + + return null; + + } + + /** + * 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) { + $name = static::name($string); + $extension = static::extension($string); + $end = !empty($extension) ? '.' . str::slug($extension) : ''; + return str::slug($name, '-', 'a-z0-9@._-') . $end; + } + + /** + * Checks if the file is writable + * + * @param string $file + * @return boolean + */ + public static function isWritable($file) { + return is_writable($file); + } + + /** + * Checks if the file is readable + * + * @param string $file + * @return boolean + */ + public static function isReadable($file) { + return is_readable($file); + } + + /** + * Read and send the file with the correct headers + * + * @param string $file + */ + public static function show($file) { + + // stop the download if the file does not exist or is not readable + if(!is_file($file) || !is_readable($file)) return false; + + // send the browser headers + header::type(f::mime($file)); + + // send the file + die(f::read($file)); + + } + + /* + * Automatically sends all needed headers for the file to be downloaded + * and echos the file's content + * + * @param string $file The root to the file + * @param string $name Optional filename for the download + */ + public static function download($file, $name = null) { + + // stop the download if the file does not exist or is not readable + if(!is_file($file) || !is_readable($file)) return false; + + header::download(array( + 'name' => $name ? $name : f::filename($file), + 'size' => f::size($file), + 'mime' => f::mime($file), + 'modified' => f::modified($file) + )); + + die(f::read($file)); + + } + + /** + * Tries to find a file by various extensions + * + * @param string $base + * @param array $extensions + * @return string|false + */ + public static function resolve($base, $extensions) { + foreach($extensions as $ext) { + $file = $base . '.' . $ext; + if(file_exists($file)) return $file; + } + return false; + } + +} diff --git a/kirby/toolkit/lib/folder.php b/kirby/toolkit/lib/folder.php new file mode 100644 index 0000000..39e9d76 --- /dev/null +++ b/kirby/toolkit/lib/folder.php @@ -0,0 +1,406 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Folder { + + // the root for the directory + protected $root = null; + + // a cache for the scanned inventory + protected $inventory = null; + + /** + * Constructor + */ + public function __construct($root) { + if(file_exists($root) && is_file($root)) throw new Exception('Invalid folder: ' . $root); + $this->root = $root; + } + + /** + * Returns the root of the directory + */ + public function root() { + return $this->root; + } + + /** + * Returns a md5 hash of the root + */ + public function hash() { + return md5($this->root); + } + + /** + * Returns the name of the directory without the full path + * + * @return string + */ + public function name() { + return basename($this->root); + } + + /** + * Returns the parent directory object + * + * @return Directory + */ + public function parent() { + return new static(dirname($this->root)); + } + + /** + * Checks if the dir exists + * + * @return boolean + */ + public function exists() { + return is_dir($this->root); + } + + /** + * Creates the directory if it does not exist yet + * + * @param boolean $recursive + * @return boolean + */ + public function make($recursive = true) { + return dir::make($this->root, $recursive); + } + + /** + * Alternative for make + * + * @param boolean $recursive + * @return boolean + */ + public function create($recursive = true) { + return $this->make($recursive); + } + + /** + * Returns the entire content of the directory + * + * @return array + */ + public function inventory() { + if(!is_dir($this->root)) return array(); + return $this->inventory = is_null($this->inventory) ? scandir($this->root) : $this->inventory; + } + + /** + * Reads the directory content and returns an array with file objects + * + * @param array $ignore + * @return array + */ + public function scan($ignore = null) { + $skip = is_array($ignore) ? $ignore : a::get(dir::$defaults, 'ignore', array()); + return empty($skip) ? $this->inventory() : (array)array_diff($this->inventory(), $skip); + } + + /** + * Alternative for scan + * + * @param array $ignore + * @return array + */ + public function read($ignore = null) { + return $this->scan($ignore); + } + + /** + * Returns a collection with full File and Directory objects + * for each item in the directory + * + * @param array $ignore + * @return Collection + */ + public function content($ignore = null) { + $raw = $this->scan($ignore); + $root = $this->root; + $content = new Collection(); + + foreach($raw as $file) { + + if(is_dir($root . DS . $file)) { + $content->append($file, new static($root . DS . $file)); + } else { + $content->append($file, new Media($root . DS . $file)); + } + + } + + return $content; + + } + + /** + * Return a collection of all files within the directory + * + * @param array $ignore + * @param boolean $plain + * @return mixed When $plain is true an array will be returned. Otherwise a Collection + */ + public function files($ignore = null, $plain = false) { + + $raw = $this->scan($ignore); + + if($plain) { + + $content = array(); + + foreach($raw as $file) { + if(is_file($this->root . DS . $file)) $content[] = $file; + } + + } else { + + $content = new Collection(); + + foreach($raw as $file) { + if(is_file($this->root . DS . $file)) { + $content->append($file, new Media($this->root . DS . $file)); + } + } + + } + + return $content; + + } + + /** + * Return a collection of subfolders + * + * @param array $ignore + * @param boolean $plain + * @return mixed If $plain is true an array will be returned. Otherwise a Collection + */ + public function children($ignore = null, $plain = false) { + + $raw = $this->scan($ignore); + + if($plain) { + + $content = array(); + + foreach($raw as $file) { + if(is_dir($this->root . DS . $file)) $content[] = $file; + } + + } else { + + $content = new Collection(); + + foreach($raw as $file) { + if(is_dir($this->root . DS . $file)) { + $content->append($file, new static($this->root . DS . $file)); + } + } + + } + + return $content; + + } + + /** + * Returns a subfolder object by path + * + * @return mixed Directory + */ + public function child($path) { + $root = $this->root . DS . str_replace('/', DS, $path); + if(!is_dir($root)) return false; + return new static($root); + } + + /** + * Corresponding method to File::type() + * which makes it possible to filter a collection + * of files and directories by type. + * + * @return string + */ + public function type() { + return 'directory'; + } + + /** + * Moves the directory to a new location + * + * @param string $to + * @return boolean + */ + public function move($to) { + if(!dir::move($this->root, $to)) { + return false; + } else { + $this->root = true; + return true; + } + } + + /** + * Copies the directory to a new location + * + * @param string $to + * @return boolean + */ + public function copy($to) { + + // Make destination directory + $copy = new static($to); + if(!$copy->make()) return false; + + // Loop through all subfiles and folders + foreach($this->content() as $item) { + if(is_a($item, 'Folder')) { + $dest = $to . DS . $item->name(); + } else { + $dest = $to . DS . $item->filename(); + } + if(!$item->copy($dest)) return false; + } + + return $copy; + + } + + /** + * Deletes the directory + * + * @param boolean $keep Set this to true to keep the directory but delete all its content + * @return boolean + */ + public function delete($keep = false) { + $items = $this->content(array('.', '..')); + foreach($items as $item) $item->delete(); + return $keep ? true : @rmdir($this->root); + } + + /** + * Alternative for delete + * + * @param boolean $keep Set this to true to keep the directory but delete all its content + * @return boolean + */ + public function remove($keep = false) { + return $this->delete($keep); + } + + /** + * Deletes all contents of the directory + * + * @return boolean + */ + public function flush() { + return $this->delete(true); + } + + /** + * Alternative for flush + * + * @return boolean + */ + public function clean() { + return $this->delete(true); + } + + /** + * Returns the entire size of the directory and all its contents + * + * @return int + */ + public function size() { + + $size = 0; + $items = $this->content(array('.', '..')); + + foreach($items AS $item) $size += $item->size(); + return $size; + + } + + /** + * Returns the size as a human-readable string + * + * @return string + */ + public function niceSize() { + return f::niceSize($this->size()); + } + + /** + * Recursively check when the dir and all + * subfolders have been modified for the last time. + * + * @return int + */ + public function modified($format = null, $handler = 'date') { + + $modified = filemtime($this->root); + $items = $this->scan(array('.', '..')); + + foreach($items AS $item) { + + if(is_file($this->root . DS . $item)) { + $newModified = filemtime($this->root . DS . $item); + } else { + $object = new static($this->root . DS . $item); + $newModified = $object->modified(); + } + + $modified = ($newModified > $modified) ? $newModified : $modified; + + } + + return !is_null($format) ? $handler($format, $modified) : $modified; + + } + + /** + * Checks if the directory is writable + * + * @param boolean $recursive + * @return boolean + */ + public function isWritable($recursive = false) { + if($recursive) { + if(!$this->isWritable()) return false; + foreach($this->content() as $f) { + if(!$f->isWritable(true)) return false; + } + return true; + } + return is_writable($this->root); + } + + /** + * Checks if the directory is readable + * + * @return boolean + */ + public function isReadable() { + return is_readable($this->root); + } + + /** + * Makes it possible to echo the entire object + * + * @return string + */ + public function __toString() { + return $this->root; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/header.php b/kirby/toolkit/lib/header.php new file mode 100644 index 0000000..7c87220 --- /dev/null +++ b/kirby/toolkit/lib/header.php @@ -0,0 +1,236 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Header { + + // configuration + public static $codes = array( + + // successful + '_200' => 'OK', + '_201' => 'Created', + '_202' => 'Accepted', + + // redirection + '_301' => 'Moved Permanently', + '_302' => 'Found', + '_303' => 'See Other', + '_304' => 'Not Modified', + '_307' => 'Temporary Redirect', + + // client error + '_400' => 'Bad Request', + '_401' => 'Unauthorized', + '_402' => 'Payment Required', + '_403' => 'Forbidden', + '_404' => 'Not Found', + '_405' => 'Method Not Allowed', + + // server error + '_500' => 'Internal Server Error', + '_501' => 'Not Implemented', + '_502' => 'Bad Gateway', + '_503' => 'Service Unavailable' + ); + + /** + * Sends a content type header + * + * @param string $mime + * @param string $charset + * @param boolean $send + * @return mixed + */ + public static function contentType($mime, $charset = 'UTF-8', $send = true) { + if(f::extensionToMime($mime)) $mime = f::extensionToMime($mime); + $header = 'Content-type: ' . $mime; + if($charset) $header .= '; charset=' . $charset; + if(!$send) return $header; + header($header); + } + + /** + * Shortcut for static::contentType() + * + * @param string $mime + * @param string $charset + * @param boolean $send + * @return mixed + */ + public static function type($mime, $charset = 'UTF-8', $send = true) { + return static::contentType($mime, $charset, $send); + } + + /** + * Sends a status header + * + * @param int $code The HTTP status code + * @param boolean $send If set to false the header will be returned instead + * @return mixed + */ + public static function status($code, $send = true) { + + $codes = static::$codes; + $code = !array_key_exists('_' . $code, $codes) ? 400 : $code; + $message = isset($codes['_' . $code]) ? $codes['_' . $code] : 'Something went wrong'; + $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0'; + $header = $protocol . ' ' . $code . ' ' . $message; + + if(!$send) return $header; + + // try to send the header + header($header); + + } + + /** + * Sends a 200 header + * + * @param boolean $send + * @return mixed + */ + public static function success($send = true) { + return static::status(200, $send); + } + + /** + * Sends a 201 header + * + * @param boolean $send + * @return mixed + */ + public static function created($send = true) { + return static::status(201, $send); + } + + /** + * Sends a 202 header + * + * @param boolean $send + * @return mixed + */ + public static function accepted($send = true) { + return static::status(202, $send); + } + + /** + * Sends a 400 header + * + * @param boolean $send + * @return mixed + */ + public static function error($send = true) { + return static::status(400, $send); + } + + /** + * Sends a 403 header + * + * @param boolean $send + * @return mixed + */ + public static function forbidden($send = true) { + return static::status(403, $send); + } + + /** + * Sends a 404 header + * + * @param boolean $send + * @return mixed + */ + public static function notfound($send = true) { + return static::status(404, $send); + } + + /** + * Sends a 404 header + * + * @param boolean $send + * @return mixed + */ + public static function missing($send = true) { + return static::status(404, $send); + } + + /** + * Sends a 500 header + * + * @param boolean $send + * @return mixed + */ + public static function panic($send = true) { + return static::status(500, $send); + } + + /** + * Sends a 503 header + * + * @param boolean $send + * @return mixed + */ + public static function unavailable($send = true) { + return static::status(503, $send); + } + + /** + * Sends a redirect header + * + * @param boolean $send + * @return mixed + */ + public static function redirect($url, $code = 301, $send = true) { + + $status = static::status($code, false); + $location = 'Location:' . $url; + + if(!$send) { + return $status . PHP_EOL . $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($params = array()) { + + $defaults = array( + 'name' => 'download', + 'size' => false, + 'mime' => 'application/force-download', + 'modified' => time() + ); + + $options = array_merge($defaults, $params); + + header('Pragma: public'); + header('Expires: 0'); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Last-Modified: '. gmdate('D, d M Y H:i:s', $options['modified']) . ' GMT'); + header('Cache-Control: private', false); + static::contentType($options['mime']); + header('Content-Disposition: attachment; filename="' . $options['name'] . '"'); + header('Content-Transfer-Encoding: binary'); + if($options['size']) header('Content-Length: ' . $options['size']); + header('Connection: close'); + + } + +} diff --git a/kirby/toolkit/lib/html.php b/kirby/toolkit/lib/html.php new file mode 100644 index 0000000..9db770e --- /dev/null +++ b/kirby/toolkit/lib/html.php @@ -0,0 +1,255 @@ + + * @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 + * + * @return array + */ + public static $entities = array( + ' ' => ' ', '¡' => '¡', '¢' => '¢', '£' => '£', '¤' => '¤', '¥' => '¥', '¦' => '¦', '§' => '§', + '¨' => '¨', '©' => '©', 'ª' => 'ª', '«' => '«', '¬' => '¬', '­' => '­', '®' => '®', '¯' => '¯', + '°' => '°', '±' => '±', '²' => '²', '³' => '³', '´' => '´', 'µ' => 'µ', '¶' => '¶', '·' => '·', + '¸' => '¸', '¹' => '¹', 'º' => 'º', '»' => '»', '¼' => '¼', '½' => '½', '¾' => '¾', '¿' => '¿', + 'À' => 'À', 'Á' => 'Á', 'Â' => 'Â', 'Ã' => 'Ã', 'Ä' => 'Ä', 'Å' => 'Å', 'Æ' => 'Æ', 'Ç' => 'Ç', + 'È' => 'È', 'É' => 'É', 'Ê' => 'Ê', 'Ë' => 'Ë', 'Ì' => 'Ì', 'Í' => 'Í', 'Î' => 'Î', 'Ï' => 'Ï', + 'Ð' => 'Ð', 'Ñ' => 'Ñ', 'Ò' => 'Ò', 'Ó' => 'Ó', 'Ô' => 'Ô', 'Õ' => 'Õ', 'Ö' => 'Ö', '×' => '×', + 'Ø' => 'Ø', 'Ù' => 'Ù', 'Ú' => 'Ú', 'Û' => 'Û', 'Ü' => 'Ü', 'Ý' => 'Ý', 'Þ' => 'Þ', 'ß' => 'ß', + 'à' => 'à', 'á' => 'á', 'â' => 'â', 'ã' => 'ã', 'ä' => 'ä', 'å' => 'å', 'æ' => 'æ', 'ç' => 'ç', + 'è' => 'è', 'é' => 'é', 'ê' => 'ê', 'ë' => 'ë', 'ì' => 'ì', 'í' => 'í', 'î' => 'î', 'ï' => 'ï', + 'ð' => 'ð', 'ñ' => 'ñ', 'ò' => 'ò', 'ó' => 'ó', 'ô' => 'ô', 'õ' => 'õ', 'ö' => 'ö', '÷' => '÷', + 'ø' => 'ø', 'ù' => 'ù', 'ú' => 'ú', 'û' => 'û', 'ü' => 'ü', 'ý' => 'ý', 'þ' => 'þ', 'ÿ' => 'ÿ', + 'ƒ' => 'ƒ', 'Α' => 'Α', 'Β' => 'Β', 'Γ' => 'Γ', 'Δ' => 'Δ', 'Ε' => 'Ε', 'Ζ' => 'Ζ', 'Η' => 'Η', + 'Θ' => 'Θ', 'Ι' => 'Ι', 'Κ' => 'Κ', 'Λ' => 'Λ', 'Μ' => 'Μ', 'Ν' => 'Ν', 'Ξ' => 'Ξ', 'Ο' => 'Ο', + 'Π' => 'Π', 'Ρ' => 'Ρ', 'Σ' => 'Σ', 'Τ' => 'Τ', 'Υ' => 'Υ', 'Φ' => 'Φ', 'Χ' => 'Χ', 'Ψ' => 'Ψ', + 'Ω' => 'Ω', 'α' => 'α', 'β' => 'β', 'γ' => 'γ', 'δ' => 'δ', 'ε' => 'ε', 'ζ' => 'ζ', 'η' => 'η', + 'θ' => 'θ', 'ι' => 'ι', 'κ' => 'κ', 'λ' => 'λ', 'μ' => 'μ', 'ν' => 'ν', 'ξ' => 'ξ', 'ο' => 'ο', + 'π' => 'π', 'ρ' => 'ρ', 'ς' => 'ς', 'σ' => 'σ', 'τ' => 'τ', 'υ' => 'υ', 'φ' => 'φ', 'χ' => 'χ', + 'ψ' => 'ψ', 'ω' => 'ω', 'ϑ' => 'ϑ', 'ϒ' => 'ϒ', 'ϖ' => 'ϖ', '•' => '•', '…' => '…', '′' => '′', + '″' => '″', '‾' => '‾', '⁄' => '⁄', '℘' => '℘', 'ℑ' => 'ℑ', 'ℜ' => 'ℜ', '™' => '™', 'ℵ' => 'ℵ', + '←' => '←', '↑' => '↑', '→' => '→', '↓' => '↓', '↔' => '↔', '↵' => '↵', '⇐' => '⇐', '⇑' => '⇑', + '⇒' => '⇒', '⇓' => '⇓', '⇔' => '⇔', '∀' => '∀', '∂' => '∂', '∃' => '∃', '∅' => '∅', '∇' => '∇', + '∈' => '∈', '∉' => '∉', '∋' => '∋', '∏' => '∏', '∑' => '∑', '−' => '−', '∗' => '∗', '√' => '√', + '∝' => '∝', '∞' => '∞', '∠' => '∠', '∧' => '∧', '∨' => '∨', '∩' => '∩', '∪' => '∪', '∫' => '∫', + '∴' => '∴', '∼' => '∼', '≅' => '≅', '≈' => '≈', '≠' => '≠', '≡' => '≡', '≤' => '≤', '≥' => '≥', + '⊂' => '⊂', '⊃' => '⊃', '⊄' => '⊄', '⊆' => '⊆', '⊇' => '⊇', '⊕' => '⊕', '⊗' => '⊗', '⊥' => '⊥', + '⋅' => '⋅', '⌈' => '⌈', '⌉' => '⌉', '⌊' => '⌊', '⌋' => '⌋', '⟨' => '〈', '⟩' => '〉', '◊' => '◊', + '♠' => '♠', '♣' => '♣', '♥' => '♥', '♦' => '♦', '"' => '"', '&' => '&', '<' => '<', '>' => '>', 'Œ' => 'Œ', + 'œ' => 'œ', 'Š' => 'Š', 'š' => 'š', 'Ÿ' => 'Ÿ', 'ˆ' => 'ˆ', '˜' => '˜', ' ' => ' ', ' ' => ' ', + ' ' => ' ', '‌' => '‌', '‍' => '‍', '‎' => '‎', '‏' => '‏', '–' => '–', '—' => '—', '‘' => '‘', + '’' => '’', '‚' => '‚', '“' => '“', '”' => '”', '„' => '„', '†' => '†', '‡' => '‡', '‰' => '‰', + '‹' => '‹', '›' => '›', '€' => '€' + ); + + /** + * Checks if a tag is self-closing + * + * @param string $tag + * @return param + */ + public static function isVoid($tag) { + + $void = array( + 'area', + 'base', + 'br', + 'col', + 'command', + 'embed', + 'hr', + 'img', + 'input', + 'keygen', + 'link', + 'meta', + 'param', + 'source', + 'track', + 'wbr', + ); + + return in_array(strtolower($tag), $void); + + } + + /** + * Returns the full array with all HTML entities + * + * @return array + */ + public static function entities() { + return static::$entities; + } + + /** + * Converts a string to a html-safe string + * + * @param string $string + * @param boolean $keepTags True: lets stuff inside html tags untouched. + * @return string The html string + */ + public static function encode($string, $keepTags = true) { + if($keepTags) { + return stripslashes(implode('', preg_replace_callback('/^([^<].+[^>])$/', function($match) { + return htmlentities($match[1], ENT_COMPAT, 'utf-8'); + }, preg_split('/(<.+?>)/', $string, -1, PREG_SPLIT_DELIM_CAPTURE)))); + } else { + return htmlentities($string, ENT_COMPAT, 'utf-8'); + } + } + + /** + * Removes all html tags and encoded chars from a string + * + * + * + * echo html::decode('some crazy stuff'); + * // output: some uber crazy stuff + * + * + * + * @param string $string + * @return string The html string + */ + public static function decode($string) { + $string = strip_tags($string); + return html_entity_decode($string, ENT_COMPAT, 'utf-8'); + } + + /** + * Converts lines in a string into html breaks + * + * @param string $string + * @return string + */ + public static function breaks($string) { + return nl2br($string); + } + + /** + * 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($name, $content = null, $attr = array()) { + + if(is_array($content)) { + $attr = $content; + $content = null; + } + + $html = '<' . $name; + $attr = static::attr($attr); + + if(!empty($attr)) $html .= ' ' . $attr; + + if(static::isVoid($name)) { + $html .= '>'; + } else { + $html .= '>' . $content . ''; + } + + return $html; + + } + + /** + * 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) { + if(is_array($name)) { + $attributes = array(); + foreach($name as $key => $val) { + $a = static::attr($key, $val); + if($a) $attributes[] = $a; + } + return implode(' ', $attributes); + } + + if(empty($value) && $value !== '0' && $value !== 0) { + return false; + } else if($value === ' ') { + return strtolower($name) . '=""'; + } else if(is_bool($value)) { + return $value === true ? strtolower($name) : ''; + } else { + return strtolower($name) . '="' . ( is_array($value) ? implode(' ', $value) : $value ) . '"'; + } + + } + + /** + * 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($href, $text = null, $attr = array()) { + $attr = array_merge(array('href' => $href), $attr); + if(empty($text)) $text = $href; + return static::tag('a', $text, $attr); + } + + /** + * 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($email, $text = null, $attr = array()) { + if(empty($text)) { + // show only the eMail address without additional parameters (if the 'text' argument is empty) + $text = str::encode(a::first(str::split($email, '?'))); + } + $email = str::encode($email); + $attr = array_merge(array('href' => 'mailto:' . $email), $attr); + return static::tag('a', $text, $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($src, $attr = array()) { + $attr = array_merge(array('src' => $src, 'alt' => pathinfo($src, PATHINFO_FILENAME)), $attr); + return static::tag('img', null, $attr); + } + + /** + * Generates a HTML5 shiv script tag with additional comments for older IEs + * + * @return string the generated html + */ + public static function shiv() { + return '' . PHP_EOL; + } + +} diff --git a/kirby/toolkit/lib/i.php b/kirby/toolkit/lib/i.php new file mode 100644 index 0000000..59de3fe --- /dev/null +++ b/kirby/toolkit/lib/i.php @@ -0,0 +1,105 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class I implements Iterator { + + public $data = array(); + + /** + * Constructor + * + * @param array $data + */ + public function __construct($data = array()) { + if(is_array($data)) $this->data = $data; + } + + /** + * Checks if the current key is set + * + * `isset($mycollection->mykey)` + * + * @param string $key the key to check + * @return boolean + */ + public function __isset($key) { + return isset($this->data[$key]); + } + + /** + * Removes an element from the array by key + * + * `unset($mycollection->mykey)` + * + * @param string $key the name of the key + */ + public function __unset($key) { + unset($this->data[$key]); + } + + /** + * Moves the cusor to the first element of the array + */ + public function rewind() { + reset($this->data); + } + + /** + * Returns the current element of the array + * + * @return mixed + */ + public function current() { + return current($this->data); + } + + /** + * Returns the current key from the array + * + * @return string + */ + public function key() { + return key($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); + } + + /** + * Checks if an element is valid + * This is needed for the Iterator implementation + * + * @return boolean + */ + public function valid() { + return $this->current() !== false; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/l.php b/kirby/toolkit/lib/l.php new file mode 100644 index 0000000..7c607eb --- /dev/null +++ b/kirby/toolkit/lib/l.php @@ -0,0 +1,17 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class L extends Silo { + public static $data = array(); +} \ No newline at end of file diff --git a/kirby/toolkit/lib/media.php b/kirby/toolkit/lib/media.php new file mode 100644 index 0000000..b5c4e9c --- /dev/null +++ b/kirby/toolkit/lib/media.php @@ -0,0 +1,639 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Media { + + // optional url where the file is reachable + public $url = null; + + // the full path for the file + protected $root = null; + + // the filename including the extension + protected $filename = null; + + // the name excluding the extension + protected $name = null; + + // the extension of the file + protected $extension = null; + + // the content of the file + protected $content = null; + + // cache for various data + protected $cache = array(); + + /** + * Constructor + * + * @param string $root + */ + public function __construct($root, $url = null) { + $this->url = $url; + $this->root = $root === null ? $root : realpath($root); + $this->filename = basename($root); + $this->name = pathinfo($root, PATHINFO_FILENAME); + $this->extension = strtolower(pathinfo($root, PATHINFO_EXTENSION)); + } + + /** + * Resets the internal cache + */ + public function reset() { + $this->cache = array(); + } + + /** + * Returns the full root of the asset + * + * @return string + */ + public function root() { + return $this->root; + } + + /** + * Returns the url + * + * @return string + */ + public function url() { + return $this->url; + } + + /** + * Returns a md5 hash of the root + */ + public function hash() { + return md5($this->root); + } + + /** + * Returns the parent directory path + * + * @return string + */ + public function dir() { + return dirname($this->root); + } + + /** + * Returns the filename of the file + * i.e. somefile.jpg + * + * @return string + */ + public function filename() { + return $this->filename; + } + + /** + * Returns the name of the file without extension + * + * @return string + */ + public function name() { + return $this->name; + } + + /** + * Returns the filename as safe name + * + * @return string + */ + public function safeName() { + return f::safeName($this->filename()); + } + + /** + * Returns the extension of the file + * i.e. jpg + * + * @return string + */ + public function extension() { + // return the current extension + return $this->extension; + } + + /** + * Reads the file content and parses it + * + * @param string $format + * @return mixed + */ + public function read($format = null) { + return str::parse($this->content(), $format); + } + + /** + * Setter and getter for the file content + * + * @param string $content + * @return string + */ + public function content($content = null, $format = null) { + + if(!is_null($content)) { + if(is_array($content)) { + switch($format) { + case 'json': + $content = json_encode($content); + break; + case 'yaml': + $content = yaml::encode($content); + break; + default: + $content = serialize($content); + break; + } + } else if(is_object($content)) { + $content = serialize($content); + } + return $this->content = $content; + } + + if(is_null($this->content)) { + $this->content = file_get_contents($this->root); + } + + return $this->content; + + } + + /** + * Saves the file + * + * @param string $content + * @return boolean + */ + public function save($content = null, $format = null) { + $content = $this->content($content, $format); + return f::write($this->root, $content); + } + + /** + * Alternative for save + * + * @param string $content + * @return boolean + */ + public function write($content = null, $format = null) { + return $this->save($content, $format); + } + + /** + * Change the file's modification date to now + * and create it with an empty content if it is not there yet + * + * @return boolean + */ + public function touch() { + return touch($this->root); + } + + /** + * Appends the content and saves the file + * + * @param string $content + * @return boolean + */ + public function append($content) { + $this->content = $this->content() . $content; + return $this->save(); + } + + /** + * Deletes the file + * + * @return boolean + */ + public function delete() { + return f::remove($this->root); + } + + /** + * Alternative for delete + * + * @return boolean + */ + public function remove() { + return f::remove($this->root); + } + + /** + * Moves the file to a new location + * + * @param string $to + * @return boolean + */ + public function move($to) { + if(!f::move($this->root, $to)) { + return false; + } else { + $this->root = $to; + return true; + } + } + + /** + * Copies the file to a new location + * + * @param string $to + * @return boolean + */ + public function copy($to) { + return f::copy($this->root, $to); + } + + /** + * Returns the file size as integer + * + * @return int + */ + public function size() { + return f::size($this->root); + } + + /** + * Returns the human readable version of the file size + * + * @return string + */ + public function niceSize() { + return f::niceSize($this->size()); + } + + /** + * Get the file's last modification time. + * + * @return int + */ + public function modified($format = null, $handler = 'date') { + return f::modified($this->root, $format, $handler); + } + + /** + * Returns the mime type of a file + * + * @return string + */ + public function mime() { + return f::mime($this->root); + } + + /** + * Categorize the file + * + * @return string + */ + public function type() { + return f::type($this->root); + } + + /** + * Checks if a file is of a certain type + * + * @param string $value An extension or mime type + * @return boolean + */ + public function is($value) { + return f::is($this->root, $value); + } + + /** + * Returns the file content as base64 encoded string + * + * @return string + */ + public function base64() { + return base64_encode($this->content()); + } + + /** + * Returns the file as data uri + * + * @return string + */ + public function dataUri() { + return 'data:' . $this->mime() . ';base64,' . $this->base64(); + } + + /** + * Checks if the file exists + * + * @return boolean + */ + public function exists() { + return file_exists($this->root); + } + + /** + * Checks if the file is writable + * + * @return boolean + */ + public function isWritable() { + return is_writable($this->root); + } + + /** + * Checks if the file is readable + * + * @return boolean + */ + public function isReadable() { + return is_readable($this->root); + } + + /** + * Checks if the file is executable + * + * @return boolean + */ + public function isExecutable() { + return is_executable($this->root); + } + + /** + * Sends an appropriate header for the asset + * + * @param boolean $send + * @return mixed + */ + public function header($send = true) { + return header::type($this->mime(), false, $send); + } + + /** + * Safely requires a file if it exists + * + * @param array $data Optional variables, which will be made available to the file + */ + public function load($data = array()) { + return f::load($this->root, $data); + } + + /** + * Read and send the file with the correct headers + */ + public function show() { + f::show($this->root); + } + + /* + * Automatically sends all needed headers for the file to be downloaded + * and echos the file's content + * + * @param string $filename Optional filename for the download + */ + public function download($filename = null) { + f::download($this->root, $filename); + } + + /** + * Returns the exif object for this file (if image) + * + * @return Exif + */ + public function exif() { + if(isset($this->cache['exif'])) return $this->cache['exif']; + return $this->cache['exif'] = new Exif($this); + } + + /** + * Returns the PHP imagesize array + * + * @return array + */ + public function imagesize() { + return (array)getimagesize($this->root); + } + + /** + * Returns the dimensions of the file if possible + * + * @return Dimensions + */ + public function dimensions() { + + if(isset($this->cache['dimensions'])) return $this->cache['dimensions']; + + if(in_array($this->mime(), array('image/jpeg', 'image/png', 'image/gif'))) { + $size = (array)getimagesize($this->root); + $width = a::get($size, 0, 0); + $height = a::get($size, 1, 0); + } else if($this->extension() == 'svg') { + $content = $this->read(); + $xml = simplexml_load_string($content); + $attr = $xml->attributes(); + $width = floatval($attr->width); + $height = floatval($attr->height); + if($width == 0 or $height == 0 and !empty($attr->viewBox)) { + $box = str::split($attr->viewBox, ' '); + $width = floatval(a::get($box, 2, 0)); + $height = floatval(a::get($box, 3, 0)); + } + } else { + $width = 0; + $height = 0; + } + + return $this->cache['dimensions'] = new Dimensions($width, $height); + + } + + /** + * Returns the width of the asset + * + * @return int + */ + public function width() { + return $this->dimensions()->width(); + } + + /** + * Returns the height of the asset + * + * @return int + */ + public function height() { + return $this->dimensions()->height(); + } + + /** + * Returns the ratio of the asset + * + * @return int + */ + public function ratio() { + return $this->dimensions()->ratio(); + } + + /** + * Checks if the dimensions of the asset are portrait + * + * @return boolean + */ + public function isPortrait() { + return $this->dimensions()->portrait(); + } + + /** + * Checks if the dimensions of the asset are landscape + * + * @return boolean + */ + public function isLandscape() { + return $this->dimensions()->landscape(); + } + + /** + * Checks if the dimensions of the asset are square + * + * @return boolean + */ + public function isSquare() { + return $this->dimensions()->square(); + } + + /** + * Returns the orientation as string + * landscape | portrait | square + * + * @return string + */ + public function orientation() { + return $this->dimensions()->orientation(); + } + + /** + * @param array $attr + * @return string + */ + public function html($attr = array()) { + + if($this->type() != 'image') return false; + + $img = new Brick('img'); + $img->attr('src', $this->url()); + $img->attr('alt', ' '); + + if(is_string($attr) || (is_object($attr) && method_exists($attr, '__toString'))) { + $img->attr('alt', (string)$attr); + } else if(is_array($attr)) { + $img->attr($attr); + } + + return $img; + + } + + /** + * Scales the image if possible + * + * @param int $width + * @param mixed $height + * @param mixed $quality + * @return Media + */ + public function resize($width, $height = null, $quality = null) { + + if($this->type() != 'image') return $this; + + $params = array('width' => $width); + + if($height) $params['height'] = $height; + if($quality) $params['quality'] = $quality; + + return new Thumb($this, $params); + + } + + /** + * Scales and crops the image if possible + * + * @param int $width + * @param mixed $height + * @param mixed $quality + * @return Media + */ + public function crop($width, $height = null, $quality = null) { + + if($this->type() != 'image') return $this; + + $params = array('width' => $width, 'crop' => true); + + if($height) $params['height'] = $height; + if($quality) $params['quality'] = $quality; + + return new Thumb($this, $params); + + } + + /** + * Converts the media object to a + * plain PHP array + * + * @param closure $callback + * @return array + */ + public function toArray($callback = null) { + + $data = array( + 'root' => $this->root(), + 'url' => $this->url(), + 'hash' => $this->hash(), + 'dir' => $this->dir(), + 'filename' => $this->filename(), + 'name' => $this->name(), + 'safeName' => $this->safeName(), + 'extension' => $this->extension(), + 'size' => $this->size(), + 'niceSize' => $this->niceSize(), + 'modified' => $this->modified(), + 'mime' => $this->mime(), + 'type' => $this->type(), + 'dimensions' => $this->dimensions()->toArray() + ); + + + if(is_null($callback)) { + return $data; + } else { + return array_map($callback, $data); + } + + } + + /** + * Converts the entire file array into + * a json string + * + * @param closure $callback Filter callback + * @return string + */ + public function toJson($callback = null) { + return json_encode($this->toArray($callback)); + } + + /** + * Returns a full link to this file + * Perfect for debugging in connection with echo + * + * @return string + */ + public function __toString() { + return $this->root; + } + +} diff --git a/kirby/toolkit/lib/obj.php b/kirby/toolkit/lib/obj.php new file mode 100644 index 0000000..2ed253c --- /dev/null +++ b/kirby/toolkit/lib/obj.php @@ -0,0 +1,38 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Obj extends stdClass { + + public function __construct($data = array()) { + foreach($data as $key => $val) { + $this->{$key} = $val; + } + } + + public function __call($method, $arguments) { + return isset($this->$method) ? $this->$method : null; + } + + public function set($key, $value) { + $this->$key = $value; + } + + public function get($key, $default = null) { + return isset($this->$key) ? $this->$key : $default; + } + + public function toArray() { + return (array)$this; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/pagination.php b/kirby/toolkit/lib/pagination.php new file mode 100644 index 0000000..be94b2f --- /dev/null +++ b/kirby/toolkit/lib/pagination.php @@ -0,0 +1,417 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Pagination { + + // configuration + public static $defaults = array( + 'variable' => 'page', + 'method' => 'param', + 'omitFirstPage' => true, + 'page' => false, + 'url' => null, + 'redirect' => false, + ); + + // options + protected $options = array(); + + // the current page + protected $page = null; + + // total count of items + protected $count = 0; + + // the number of displayed rows + protected $limit = 0; + + // the total number of pages + protected $pages = 0; + + // the offset for the slice function + protected $offset = 0; + + // the range start for ranged pagination + protected $rangeStart = 0; + + // the range end for ranged pagination + protected $rangeEnd = 0; + + /** + * Constructor + * + * @param mixed $count Either an integer or a Collection + * @param int $limit The number of items per page + * @param array $params Additional parameters to control the pagination object + */ + public function __construct($count, $limit, $params = array()) { + + // You can still pass an entire collection + if($count instanceof Collection) { + $count = $count->count(); + } + + $this->options = array_merge(static::$defaults, $params); + $this->count = (int)$count; + $this->limit = (int)$limit; + $this->pages = (int)ceil($this->count / $this->limit); + $this->offset = (int)(($this->page()-1) * $this->limit); + + } + + /** + * Returns the current page number + * + * @return int + */ + public function page() { + + if(!is_null($this->page)) return $this->page; + + if($this->options['page']) { + $this->page = $this->options['page']; + } else { + $this->page = ($this->options['method'] == 'query') ? get($this->options['variable']) : param($this->options['variable']); + } + + // make sure the page is an int + $this->page = intval($this->page); + + // set the first page correctly + if($this->page == 0) { + $this->page = 1; + } + + // sanitize the page if too low + if($this->page < 1) { + $this->redirect(); + $this->page = 1; + } + + // sanitize the page if too high + if($this->page > $this->pages && $this->count > 0) { + $this->redirect(); + $this->page = $this->lastPage(); + } + + // return the sanitized page number + return $this->page; + + } + + /** + * Redirects to an error page if the redirect option is set + * and the pagination is beyond the allowed boundaries + */ + public function redirect() { + if($redirect = $this->options['redirect']) { + go($redirect); + } + } + + /** + * Returns the total number of pages + * + * @return int + */ + public function countPages() { + return $this->pages; + } + + /** + * Alternative for countPages() + * + * @return int + */ + public function pages() { + return $this->pages; + } + + /** + * Returns the current offset + * This is used for the slice() method together with + * the limit to get the correct items from collections + * + * @return int + */ + public function offset() { + return $this->offset; + } + + /** + * Returns the chosen limit + * This is used for the slice() method together with + * the offset to get the correct items from collections + * + * @return int + */ + public function limit() { + return $this->limit; + } + + /** + * Checks if multiple pages are needed + * or if the collection can be displayed on a single page + * + * @return boolean + */ + public function hasPages() { + return $this->countPages() > 1; + } + + /** + * Returns the total number of items in the collection + * + * @return int + */ + public function countItems() { + return $this->count; + } + + /** + * Alternative for countItems() + * + * @return int + */ + public function items() { + return $this->count; + } + + /** + * Returns a page url for any given page number + * + * @param int $page The page number + * @return string The url + */ + public function pageURL($page) { + + if($this->options['method'] == 'query') { + + $query = url::query($this->options['url']); + + if($page == 1 && $this->options['omitFirstPage']) { + unset($query[$this->options['variable']]); + } else { + $query[$this->options['variable']] = $page; + } + + return url::build(array('query' => $query), $this->options['url']); + + } else { + + $params = url::params($this->options['url']); + + if($page == 1 && $this->options['omitFirstPage']) { + unset($params[$this->options['variable']]); + } else { + $params[$this->options['variable']] = $page; + } + + return url::build(array('params' => $params), $this->options['url']); + + } + + } + + /** + * Returns the number of the first page + * + * @return int + */ + public function firstPage() { + return 1; + } + + /** + * Checks if the current page is the first page + * + * @return boolean + */ + public function isFirstPage() { + return ($this->page == $this->firstPage()) ? true : false; + } + + /** + * Returns the url for the first page + * + * @return string + */ + public function firstPageURL() { + return $this->pageURL(1); + } + + /** + * Returns the number of the last page + * + * @return int + */ + public function lastPage() { + return $this->pages; + } + + /** + * Checks if the current page is the last page + * + * @return boolean + */ + public function isLastPage() { + return $this->page == $this->lastPage(); + } + + /** + * Returns the url for the last page + * + * @return string + */ + public function lastPageURL() { + return $this->pageURL($this->lastPage()); + } + + /** + * Returns the number of the previous page + * + * @return int + */ + public function prevPage() { + return $this->hasPrevPage() ? $this->page-1 : $this->page; + } + + /** + * Returns the url for the previous page + * + * @return string + */ + public function prevPageURL() { + return $this->pageURL($this->prevPage()); + } + + /** + * Checks if there's a previous page + * + * @return boolean + */ + public function hasPrevPage() { + return $this->page > 1; + } + + /** + * Returns the number of the next page + * + * @return int + */ + public function nextPage() { + return $this->hasNextPage() ? $this->page+1 : $this->page; + } + + /** + * Returns the url for the next page + * + * @return string + */ + public function nextPageURL() { + return $this->pageURL($this->nextPage()); + } + + /** + * Checks if there's a next page + * + * @return boolean + */ + public function hasNextPage() { + return $this->page < $this->pages; + } + + /** + * Returns the index number of the first item on the current page + * Can be used to display something like + * "Currently showing 1 - 10 of 123 items" + * + * @return int + */ + public function numStart() { + return $this->offset+1; + } + + /** + * Returns the index number of the last item on the current page + * Can be used to display something like + * "Currently showing 1 - 10 of 123 items" + * + * @return int + */ + public function numEnd() { + $end = $this->offset+$this->limit; + if($end > $this->items()) $end = $this->items(); + return $end; + } + + /** + * Creates a range of page numbers for Google-like pagination + * + * @return array + */ + public function range($range=5) { + + if($this->countPages() <= $range) { + $this->rangeStart = 1; + $this->rangeEnd = $this->countPages(); + return range($this->rangeStart, $this->rangeEnd); + } + + $this->rangeStart = $this->page - (int)floor($range/2); + $this->rangeEnd = $this->page + (int)floor($range/2); + + if($this->rangeStart <= 0) { + $this->rangeEnd += abs($this->rangeStart)+1; + $this->rangeStart = 1; + } + + if($this->rangeEnd > $this->countPages()) { + $this->rangeStart -= $this->rangeEnd-$this->countPages(); + $this->rangeEnd = $this->countPages(); + } + + return range($this->rangeStart,$this->rangeEnd); + + } + + /** + * Returns the first page of the created range + * + * @return int + */ + public function rangeStart() { + return $this->rangeStart; + } + + /** + * Returns the last page of the created range + * + * @return int + */ + public function rangeEnd() { + return $this->rangeEnd; + } + + /** + * Returns the most important pagination data into a readable array + * + * @return array + */ + public function toArray() { + return array( + 'page' => $this->page(), + 'pages' => $this->pages(), + 'items' => $this->countItems(), + ); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/password.php b/kirby/toolkit/lib/password.php new file mode 100644 index 0000000..2fd1bb0 --- /dev/null +++ b/kirby/toolkit/lib/password.php @@ -0,0 +1,48 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Password { + + /** + * Generates a salted hash for a plaintext password + * + * @param string $plaintext + * @return string + */ + public static function hash($plaintext) { + $salt = substr(str_replace('+', '.', base64_encode(sha1(str::random(), true))), 0, 22); + return crypt($plaintext, '$2a$10$' . $salt); + } + + /** + * Checks if a given string is already a hash + * + * @param string + * @return boolean + */ + public static function isHash($hash) { + return preg_match('!^\$2a\$10\$!', $hash); + } + + /** + * Checks if a password matches the encrypted hash + * + * @param string $plaintext + * @param string $hash + * @return boolean + */ + public static function match($plaintext, $hash) { + return crypt($plaintext, $hash) === $hash; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/r.php b/kirby/toolkit/lib/r.php new file mode 100644 index 0000000..f5e4c9b --- /dev/null +++ b/kirby/toolkit/lib/r.php @@ -0,0 +1,346 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class R { + + // Stores the raw request data + protected static $raw = null; + + // Stores all sanitized request data + protected static $data = null; + + // the request body + protected static $body = null; + + /** + * Returns the raw request data + * + * @return array + */ + public static function raw() { + if(!is_null(static::$raw)) return static::$raw; + return static::$raw = array_merge($_GET, $_POST); + } + + /** + * Returns either the entire data array or parts of it + * + * + * + * echo r::data('username'); + * // sample output 'bastian' + * + * echo r::data('username', 'peter'); + * // if no username is found in the request peter will be echoed + * + * + * + * @param string $key An optional key to receive only parts of the data array + * @param mixed $default A default value, which will be returned if nothing can be found for a given key + * @param mixed + */ + public static function data($key = null, $default = null) { + + if(is_null(static::$data)) { + static::$data = static::sanitize(static::raw()); + + if(!static::is('GET')) { + $body = static::body(); + parse_str($body, $parsed); + + if(!is_array($parsed)) { + $parsed = json_decode($body, false); + if(!is_array($parsed)) $parsed = array(); + } + + static::$data = array_merge($parsed, static::$data); + + } + + } + + if(is_null($key)) { + return static::$data; + } else if(isset(static::$data[$key])) { + return static::$data[$key]; + } else { + return $default; + } + + } + + /** + * Only returns get data + * + * @return array + */ + public static function getData($key = null, $default = null) { + return a::get((array)static::sanitize($_GET), $key, $default); + } + + /** + * Only returns post data + * + * @return array + */ + public static function postData($key = null, $default = null) { + return a::get((array)static::sanitize($_POST), $key, $default); + } + + /** + * Private method to sanitize incoming request data + * + * @param array $data + * @return array + */ + protected static function sanitize($data) { + + if(!is_array($data)) { + return trim(str::stripslashes($data)); + } + + foreach($data as $key => $value) { + $data[$key] = static::sanitize($value); + } + + return $data; + + } + + /** + * Sets or overwrites a variable in the data array + * + * + * + * r::set('username', 'bastian'); + * + * dump($request); + * + * // sample output: array( + * // 'username' => 'bastian' + * // ... other stuff from the request + * // ); + * + * + * + * @param mixed $key The key to set/replace. Use an array to set multiple values at once + * @param mixed $value The value + * @return array + */ + public static function set($key, $value = null) { + + // set multiple values at once + if(is_array($key)) { + foreach($key as $k => $v) static::set($k, $v); + // return this for chaining + return; + } + + // make sure the data array is actually an array + if(is_null(static::$data)) static::$data = array(); + + // sanitize the + static::$data[$key] = static::sanitize($value); + + // return the new data array + return static::$data; + + } + + /** + * Alternative for r::data($key, $default) + * + * + * + * echo r::get('username'); + * // sample output 'bastian' + * + * echo r::get('username', 'peter'); + * // if no username is found in the request peter will be echoed + * + * + * + * @param string $key An optional key to receive only parts of the data array + * @param mixed $default A default value, which will be returned if nothing can be found for a given key + * @param mixed + */ + public static function get($key = null, $default = null) { + return static::data($key, $default); + } + + /** + * Removes a variable from the request array + * + * @param string $key + */ + public static function remove($key) { + unset(static::$data[$key]); + } + + /** + * Returns the current request method + * + * @return string POST, GET, DELETE, PUT, HEAD, PATCH, etc. + */ + public static function method() { + return isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : 'GET'; + } + + /** + * Returns the request body from POST requests for example + * + * @return mixed + */ + public static function body() { + if(!is_null(static::$body)) return static::$body; + return static::$body = file_get_contents('php://input'); + } + + /** + * Returns the files array + * + * @param string $key An optional key to receive only parts of the files array + * @param mixed $default A default value, which will be returned if nothing can be found for a given key + * @return array + */ + public static function files($key = null, $default = null) { + return a::get($_FILES, $key, $default); + } + + /** + * Checks if the request is of a specific type: + * + * - GET + * - POST + * - PUT + * - PATCH + * - DELETE + * - AJAX + * + * @return boolean + */ + public static function is($method) { + if($method == 'ajax') { + return static::ajax(); + } else { + return strtoupper($method) == static::method() ? true : false; + } + } + + /** + * Checks for a specific key in the data array + * + * @return boolean + */ + public static function has($key) { + $data = static::data(); + return isset($data[$key]); + } + + /** + * Returns the referer if available + * + * + * + * echo r::referer(); + * // sample result: http://someurl.com + * + * + * + * @param string $default Pass an optional URL to use as default referer if no referer is being found + * @return string + */ + public static function referer($default = null) { + return a::get($_SERVER, 'HTTP_REFERER', $default); + } + + /** + * Nobody remembers how to spell it + * so this is a shortcut + * + * + * + * echo $request->referrer(); + * // sample result: http://someurl.com + * + * + * + * @param string $default Pass an optional URL to use as default referer if no referer is being found + * @return string + */ + public static function referrer($default = null) { + return static::referer($default); + } + + /** + * Returns the IP address from the + * request user if available + * + * @param mixed + */ + public static function ip() { + return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : false; + } + + /** + * Checks if the request has been made from the command line + * + * @return boolean + */ + public static function cli() { + return defined('STDIN') || (substr(PHP_SAPI, 0, 3) == 'cgi' && $term = getenv('TERM') && $term !== 'unknown'); + } + + /** + * Checks if the request is an AJAX request + * + * + * + * if($request->ajax()) echo 'ajax rulez'; + * + * + * + * @return boolean + */ + public static function ajax() { + return (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'); + } + + /** + * Returns the request scheme + * + * @return string + */ + public static function scheme() { + return url::scheme(); + } + + /** + * Checks if the request is encrypted + * + * @return boolean + */ + public static function ssl() { + return static::scheme() == 'https'; + } + + /** + * Alternative for r::ssl() + * + * @return boolean + */ + public static function secure() { + return static::ssl(); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/redirect.php b/kirby/toolkit/lib/redirect.php new file mode 100644 index 0000000..6a43ebd --- /dev/null +++ b/kirby/toolkit/lib/redirect.php @@ -0,0 +1,56 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Redirect { + + /** + * Redirects the user to a new URL + * + * @param string $url The URL to redirect to + * @param boolean $code The HTTP status code, which should be sent (301, 302 or 303) + * @param boolean $send If true, headers will be sent and redirection will take effect + */ + public static function send($url = false, $code = false, $send = true) { + return header::redirect($url, $code, $send); + } + + /** + * Redirects to a specific URL. You can pass either a normal URI + * a controller path or simply nothing (which redirects home) + */ + public static function to() { + static::send(call_user_func_array(array('url', 'to'), func_get_args())); + } + + /** + * Redirects to the home page of the app + */ + public static function home() { + static::send(url::home()); + } + + /** + * Redirects to the last location of the user + * + * @param string $fallback + */ + public static function back($fallback = null) { + // get the last url + $last = url::last(); + // make sure there's a proper fallback + if(empty($last)) $last = $fallback ? $fallback : url::home(); + static::send($last); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/remote.php b/kirby/toolkit/lib/remote.php new file mode 100644 index 0000000..aeae2cd --- /dev/null +++ b/kirby/toolkit/lib/remote.php @@ -0,0 +1,327 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Remote { + + // configuration + public static $defaults = array( + 'method' => 'GET', + 'data' => array(), + 'file' => null, + 'timeout' => 10, + 'headers' => array(), + 'encoding' => 'utf-8', + 'agent' => null, + 'body' => true, + ); + + // store for the response object + protected $response = null; + + // all options for the request + protected $options = array(); + + // all received headers + protected $headers = array(); + + /** + * Constructor + * + * @param string $url + * @param array $options + */ + public function __construct($url, $options = array()) { + + // set all options + $this->options = array_merge(static::$defaults, $options); + + // add the url + $this->options['url'] = $url; + + // send the request + $this->send(); + + } + + /** + * Sets up all curl options and sends the request + * + * @return object Response + */ + protected function send() { + + // start a curl request + $curl = curl_init(); + + // curl options + $params = array( + 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 => array($this, 'header'), + ); + + // add all headers + if(!empty($this->options['headers'])) $params[CURLOPT_HTTPHEADER] = $this->options['headers']; + + // add the user agent + if(!empty($this->options['agent'])) $params[CURLOPT_USERAGENT] = $this->options['agent']; + + // do some request specific stuff + switch(strtolower($this->options['method'])) { + case 'post': + $params[CURLOPT_POST] = true; + $params[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); + break; + case 'put': + + $params[CURLOPT_CUSTOMREQUEST] = 'PUT'; + $params[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); + + // put a file + if($this->options['file']) { + $params[CURLOPT_INFILE] = fopen($this->options['file'], 'r'); + $params[CURLOPT_INFILESIZE] = f::size($this->options['file']); + } + + break; + case 'delete': + $params[CURLOPT_CUSTOMREQUEST] = 'DELETE'; + $params[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); + break; + case 'head': + $params[CURLOPT_CUSTOMREQUEST] = 'HEAD'; + $params[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); + $params[CURLOPT_NOBODY] = true; + break; + } + + curl_setopt_array($curl, $params); + + $content = curl_exec($curl); + $error = curl_errno($curl); + $message = curl_error($curl); + $info = curl_getinfo($curl); + + curl_close($curl); + + $this->response = new RemoteResponse(); + $this->response->headers = $this->headers; + $this->response->error = $error; + $this->response->message = $message; + $this->response->content = $content; + $this->response->code = $info['http_code']; + $this->response->info = $info; + + return $this->response; + + } + + /** + * Used by curl to parse incoming headers + * + * @param object $curl the curl connection + * @param string $header the header line + * @return int the length of the heade + */ + protected function header($curl, $header) { + + $parts = str::split($header, ':'); + + if(!empty($parts[0]) && !empty($parts[1])) { + $this->headers[$parts[0]] = $parts[1]; + } + + return strlen($header); + + } + + /** + * Returns all options which have been + * set for the current request + * + * @return array + */ + public function options() { + return $this->options; + } + + /** + * Returns the response object for + * the current request + * + * @return object Response + */ + public function response() { + return $this->response; + } + + /** + * Static method to init this class and send a request + * + * @param string $url + * @param array $params + * @return object Response + */ + public static function request($url, $params = array()) { + $request = new self($url, $params); + return $request->response(); + } + + /** + * Static method to send a GET request + * + * @param string $url + * @param array $params + * @return object Response + */ + public static function get($url, $params = array()) { + + $defaults = array( + 'method' => 'GET', + 'data' => array(), + ); + + $options = array_merge($defaults, $params); + $query = http_build_query($options['data']); + + if(!empty($query)) { + $url = (url::hasQuery($url)) ? $url . '&' . $query : $url . '?' . $query; + } + + // remove the data array from the options + unset($options['data']); + + $request = new self($url, $options); + return $request->response(); + + } + + /** + * Static method to send a POST request + * + * @param string $url + * @param array $params + * @return object Response + */ + public static function post($url, $params = array()) { + + $defaults = array( + 'method' => 'POST' + ); + + $request = new self($url, array_merge($defaults, $params)); + return $request->response(); + + } + + /** + * Static method to send a PUT request + * + * @param string $url + * @param array $params + * @return object Response + */ + public static function put($url, $params = array()) { + + $defaults = array( + 'method' => 'PUT' + ); + + $request = new self($url, array_merge($defaults, $params)); + return $request->response(); + + } + + /** + * Static method to send a DELETE request + * + * @param string $url + * @param array $params + * @return object Response + */ + public static function delete($url, $params = array()) { + + $defaults = array( + 'method' => 'DELETE' + ); + + $request = new self($url, array_merge($defaults, $params)); + return $request->response(); + + } + + /** + * Static method to send a HEAD request + * + * @param string $url + * @param array $params + * @return object Response + */ + public static function head($url, $params = array()) { + + $defaults = array( + 'method' => 'HEAD' + ); + + $request = new self($url, array_merge($defaults, $params)); + return $request->response(); + + } + + /** + * Static method to send a HEAD request + * which only returns an array of headers + * + * @param string $url + * @param array $params + * @return array + */ + public static function headers($url, $params = array()) { + $request = static::head($url, $params); + return array_merge($request->headers(), $request->info()); + } + + /** + * 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; + } + + } + +} + +class RemoteResponse extends Obj { + + public function __toString() { + return (string)$this->content; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/response.php b/kirby/toolkit/lib/response.php new file mode 100644 index 0000000..8ffca3d --- /dev/null +++ b/kirby/toolkit/lib/response.php @@ -0,0 +1,142 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Response { + + // the response content + protected $content; + + // the format type + protected $format; + + // the HTTP code + protected $code; + + /** + * Constructor + * + * @param string $content + * @param string $format + * @param int $code Optional HTTP code + */ + public function __construct($content, $format = 'html', $code = 200) { + + $this->content = $content; + $this->format = strtolower($format); + $this->code = $code; + + // convert arrays to json + if(is_array($this->content) && $this->format == 'json') { + + if(defined('JSON_PRETTY_PRINT') && get('pretty')) { + $this->content = json_encode($this->content, JSON_PRETTY_PRINT); + } else { + $this->content = json_encode($this->content); + } + + } + + } + + /** + * Sends the correct header for the response + * + * @param boolean $send If set to false, the header will be returned + * @return mixed + */ + public function header($send = true) { + + $status = header::status($this->code, false); + $type = header::type($this->format, 'utf-8', false); + + if(!$send) return $status . PHP_EOL . $type; + + header($status); + header($type); + + } + + /** + * Returns the content of this response + * + * @return string + */ + public function content() { + return $this->content; + } + + /** + * Returns the content format + * + * @return string + */ + public function format() { + return $this->format; + } + + /** + * Returns a success response + * + * @param string $message + * @param mixed $data + * @param mixed $code + * @return object + */ + static public function success($message = 'Everything went fine', $data = array(), $code = 200) { + return new static(array( + 'status' => 'success', + 'code' => $code, + 'message' => $message, + 'data' => $data + ), 'json', $code); + } + + /** + * Returns an error response + * + * @param mixed $message Either a message string or an error or errors object + * @param mixed $code + * @param mixed $data + * @return object + */ + static public function error($message = 'Something went wrong', $code = 400, $data = array()) { + return new static(array( + 'status' => 'error', + 'code' => $code, + 'message' => $message, + 'data' => $data + ), 'json', $code); + } + + /** + * Converts an array to json and returns it properly + * + * @param array $data + * @return object + */ + static public function json($data, $code = 200) { + return new static($data, 'json', $code); + } + + /** + * Echos the content + * and sends the appropriate header + * + * @return string + */ + public function __toString() { + $this->header(); + return (string)$this->content; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/router.php b/kirby/toolkit/lib/router.php new file mode 100644 index 0000000..a286a2e --- /dev/null +++ b/kirby/toolkit/lib/router.php @@ -0,0 +1,284 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Router { + + // request instance + protected $request = null; + + // the matched route if found + protected $route = null; + + // all registered routes + protected $routes = array( + 'GET' => array(), + 'POST' => array(), + 'HEAD' => array(), + 'PUT' => array(), + 'PATCH' => array(), + 'DELETE' => array() + ); + + // The wildcard patterns supported by the router. + protected $patterns = array( + '(:num)' => '([0-9]+)', + '(:alpha)' => '([a-zA-Z]+)', + '(:any)' => '([a-zA-Z0-9\.\-_%=]+)', + '(:all)' => '(.*)', + ); + + // The optional wildcard patterns supported by the router. + protected $optional = array( + '/(:num?)' => '(?:/([0-9]+)', + '/(:alpha?)' => '(?:/([a-zA-Z]+)', + '/(:any?)' => '(?:/([a-zA-Z0-9\.\-_%=]+)', + '/(:all?)' => '(?:/(.*)', + ); + + // additional events, which can be triggered by routes + protected $filters = array(); + + /** + * Constructor + * + * @param array $routes + */ + public function __construct($routes = array()) { + $this->register($routes); + } + + /** + * Returns the found route + * + * @return mixed + */ + public function route() { + return $this->route; + } + + /** + * Returns the arguments array from the current route + * + * @return array + */ + public function arguments() { + if($route = $this->route()) return $route->arguments(); + } + + /** + * Adds a new route + * + * @param mixed $pattern + * @param mixed $params + * @param mixed $optional + * @return Obj + */ + public function register($pattern, $params = array(), $optional = array()) { + + if($pattern === false) { + return false; + } else if(is_array($pattern)) { + foreach($pattern as $v) { + if($v === false || empty($v['pattern'])) { + continue; + } else if(is_array($v['pattern'])) { + foreach($v['pattern'] as $p) { + $v['pattern'] = $p; + $this->register($p, $v); + } + } else { + $this->register($v['pattern'], $v); + } + } + return $this; + } + + $defaults = array( + 'pattern' => $pattern, + 'https' => false, + 'ajax' => false, + 'filter' => null, + 'method' => 'GET', + 'arguments' => array(), + ); + + $route = new Obj(array_merge($defaults, $params, $optional)); + + // convert single methods or methods separated by | to arrays + if(is_string($route->method)) { + + if(strpos($route->method, '|') !== false) { + $route->method = str::split($route->method, '|'); + } else if($route->method == 'ALL') { + $route->method = array_keys($this->routes); + } else { + $route->method = array($route->method); + } + + } + + if(is_string($route->filter)) { + if(strpos($route->filter, '|') !== false) { + $route->filter = str::split($route->filter, '|'); + } else { + $route->filter = array($route->filter); + } + } + + foreach($route->method as $method) { + $this->routes[strtoupper($method)][$route->pattern] = $route; + } + + return $route; + + } + + /** + * Add a new router filter + * + * @param string $name A simple name for the filter, which can be used by routes later + * @param closure $function A filter function, which should be called before routes + */ + public function filter($name, $function) { + $this->filters[$name] = $function; + } + + /** + * Return all registered filters + * + * @return array + */ + public function filters() { + return $this->filters; + } + + /** + * Call all matching filters + * + * @param mixed $filters + */ + protected function filterer($filters) { + foreach((array)$filters as $filter) { + if(array_key_exists($filter, $this->filters) && is_callable($this->filters[$filter])) { + call_user_func($this->filters[$filter]); + } + } + } + + /** + * Returns all added routes + * + * @param string $method + * @return array + */ + public function routes($method = null) { + return is_null($method) ? $this->routes : $this->routes[strtoupper($method)]; + } + + /** + * Iterate through every route to find a matching route. + * + * @param string $path Optional path to match against + * @return Route + */ + public function run($path = null) { + + $method = r::method(); + $ajax = r::ajax(); + $https = r::ssl(); + $routes = a::get($this->routes, $method, array()); + + // detect path if not set manually + if($path === null) $path = implode('/', (array)url::fragments(detect::path())); + + // empty urls should never happen + if(empty($path)) $path = '/'; + + foreach($routes as $route) { + + if($route->https && !$https) continue; + if($route->ajax && !$ajax) continue; + + // handle exact matches + if($route->pattern == $path) { + $this->route = $route; + break; + } + + // 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($route->pattern, '(') === false) continue; + + $preg = '#^'. $this->wildcards($route->pattern) . '$#u'; + + // If we get a match we'll return the route and slice off the first + // parameter match, as preg_match sets the first array item to the + // full-text match of the pattern. + if(preg_match($preg, $path, $parameters)) { + $this->route = $route; + $this->route->arguments = array_slice($parameters, 1); + break; + } + + } + + if($this->route && $this->filterer($this->route->filter) !== false) { + return $this->route; + } else { + return null; + } + + } + + /** + * Translate route URI wildcards into regular expressions. + * + * @param string $pattern + * @return string + */ + protected function wildcards($pattern) { + + $search = array_keys($this->optional); + $replace = array_values($this->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->patterns); + + } + + /** + * Find a registered route by a field and value + * + * @param string $field + * @param string $value + * @return object + */ + public function findRouteBy($field, $value) { + foreach($this->routes as $method => $routes) { + foreach($routes as $route) { + if($route->$field() == $value) return $route; + } + } + } + +} diff --git a/kirby/toolkit/lib/s.php b/kirby/toolkit/lib/s.php new file mode 100644 index 0000000..cac1749 --- /dev/null +++ b/kirby/toolkit/lib/s.php @@ -0,0 +1,291 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class S { + + public static $started = false; + public static $name = 'kirby_session'; + public static $timeout = 30; + public static $cookie = array(); + + /** + * Starts a new session + * + * + * + * s::start(); + * // do whatever you want with the session now + * + * + * + */ + public static function start() { + + if(session_status() === PHP_SESSION_ACTIVE) return true; + + // store the session name + static::$cookie += array( + 'lifetime' => 0, + 'path' => ini_get('session.cookie_path'), + 'domain' => ini_get('session.cookie_domain'), + 'secure' => r::secure(), + 'httponly' => true + ); + + // set the custom session name + session_name(static::$name); + + // make sure to use cookies only + ini_set('session.use_cookies', 1); + ini_set('session.use_only_cookies', 1); + + // try to start the session + if(!session_start()) return false; + + if(!setcookie( + static::$name, + session_id(), + cookie::lifetime(static::$cookie['lifetime']), + static::$cookie['path'], + static::$cookie['domain'], + static::$cookie['secure'], + static::$cookie['httponly'] + )) { + return false; + } + + // mark it as started + static::$started = true; + + // check if the session is still valid + if(!static::check()) { + return static::destroy(); + } + + return true; + + } + + /** + * Checks if the session is still valid + * and not expired + * + * @return boolean + */ + public static function check() { + + // check for the last activity and compare it with the session timeout + if(isset($_SESSION['kirby_session_activity']) && time() - $_SESSION['kirby_session_activity'] > static::$timeout * 60) { + return false; + } + + // check for an existing fingerprint and compare it + if(isset($_SESSION['kirby_session_fingerprint']) and $_SESSION['kirby_session_fingerprint'] !== static::fingerprint()) { + return false; + } + + // store a new fingerprint and the last activity + $_SESSION['kirby_session_fingerprint'] = static::fingerprint(); + $_SESSION['kirby_session_activity'] = time(); + + return true; + + } + + /** + * Generates a fingerprint from the user agent string + * + * @return string + */ + public static function fingerprint() { + if(!r::cli()) { + return sha1(Visitor::ua() . (ip2long($_SERVER['REMOTE_ADDR']) & ip2long('255.255.0.0'))); + } else { + return ''; + } + } + + /** + * Returns the current session id + * + * @return string + */ + public static function id() { + static::start(); + return session_id(); + } + + /** + * Sets a session value by key + * + * + * + * s::set('username', 'bastian'); + * // saves the username in the session + * + * s::set(array( + * 'key1' => 'val1', + * 'key2' => 'val2', + * 'key3' => 'val3' + * )); + * // setting multiple variables at once + * + * + * + * @param mixed $key The key to define + * @param mixed $value The value for the passed key + */ + public static function set($key, $value = false) { + + static::start(); + + if(!isset($_SESSION)) return false; + if(is_array($key)) { + $_SESSION = array_merge($_SESSION, $key); + } else { + $_SESSION[$key] = $value; + } + + } + + /** + * Gets a session value by key + * + * + * + * s::get('username', 'bastian'); + * // saves the username in the session + * + * echo s::get('username'); + * // output: 'bastian' + * + * + * + * @param mixed $key The key to look for. Pass false or null to return the entire session array. + * @param mixed $default Optional default value, which should be returned if no element has been found + * @return mixed + */ + public static function get($key = false, $default = null) { + + static::start(static::$name, static::$timeout, static::$cookie); + + if(!isset($_SESSION)) return false; + if(empty($key)) return $_SESSION; + return isset($_SESSION[$key]) ? $_SESSION[$key] : $default; + + } + + /** + * Retrieves an item and removes it afterwards + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public static function pull($key, $default = null) { + $value = s::get($key, $default); + s::remove($key); + return $value; + } + + /** + * Removes a value from the session by key + * + * + * + * $_SESSION = array( + * 'username' => 'bastian', + * 'id' => 1, + * ); + * + * s::remove('username'); + * // $_SESSION = array( + * // 'id' => 1 + * // ) + * + * + * + * @param mixed $key The key to remove by + * @return array The session array without the value + */ + public static function remove($key) { + + static::start(); + + unset($_SESSION[$key]); + return $_SESSION; + + } + + /** + * Checks if the session has already been started + * + * @return boolean + */ + public static function started() { + return static::$started; + } + + /** + * Destroys a session + * + * + * + * s::start(); + * // do whatever you want with the session now + * + * s::destroy(); + * // everything stored in the session will be deleted + * + * + * + */ + public static function destroy() { + + if(!static::$started) return false; + + $_SESSION = array(); + + cookie::remove(static::$name); + + static::$started = false; + + return session_destroy(); + + } + + /** + * Alternative for s::destroy() + */ + public static function stop() { + s::destroy(); + } + + /** + * Destroys a session first and then starts it again + */ + public static function restart() { + static::destroy(); + static::start(); + } + + /** + * Create a new session Id + */ + public static function regenerateId() { + static::start(static::$name, static::$timeout, static::$cookie); + session_regenerate_id(true); + } + +} diff --git a/kirby/toolkit/lib/server.php b/kirby/toolkit/lib/server.php new file mode 100644 index 0000000..dca04fe --- /dev/null +++ b/kirby/toolkit/lib/server.php @@ -0,0 +1,62 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Server { + + /** + * 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 = false, $default = null) { + if(empty($key)) return $_SERVER; + $key = str::upper($key); + $value = a::get($_SERVER, $key, $default); + return static::sanitize($key, $value); + } + + public static function sanitize($key, $value) { + + switch($key) { + case 'SERVER_ADDR': + case 'SERVER_NAME': + case 'HTTP_HOST': + $value = strip_tags($value); + $value = preg_replace('![^\w.:-]+!iu', '', $value); + $value = trim($value, '-'); + $value = htmlspecialchars($value); + break; + case 'SERVER_PORT': + $value = preg_replace('![^0-9]+!', '', $value); + break; + } + + return $value; + + } + +} diff --git a/kirby/toolkit/lib/silo.php b/kirby/toolkit/lib/silo.php new file mode 100644 index 0000000..ec8da16 --- /dev/null +++ b/kirby/toolkit/lib/silo.php @@ -0,0 +1,42 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Silo { + + public static $data = array(); + + public static function set($key, $value = null) { + if(is_array($key)) { + return static::$data = array_merge(static::$data, $key); + } else { + return static::$data[$key] = $value; + } + } + + public static function get($key = null, $default = null) { + if(empty($key)) return static::$data; + return isset(static::$data[$key]) ? static::$data[$key] : $default; + } + + public static function remove($key = null) { + // reset the entire array + if(is_null($key)) return static::$data = array(); + // unset a single key + unset(static::$data[$key]); + // return the array without the removed key + return static::$data; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/sql.php b/kirby/toolkit/lib/sql.php new file mode 100644 index 0000000..4c1af07 --- /dev/null +++ b/kirby/toolkit/lib/sql.php @@ -0,0 +1,806 @@ +, Lukas Bestle + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Sql { + + // list of literals which should not be escaped in queries + public static $literals = array('NOW()', null); + + // sql formatting methods, defined below + public static $methods = array(); + + // the parent database connection and database query + public $database; + public $dbquery; + + // list of bindings by sql query string that defines them + protected $bindings = array(); + + /** + * Constructor + * + * @param Database $database + * @param Database\Query $dbquery Database query that is used to set the bindings directly + */ + public function __construct($database, $dbquery = null) { + $this->database = $database; + $this->dbquery = $dbquery; + } + + /** + * Sets and returns query-specific bindings + * + * @param string $query SQL query string that contains the bindings + * @param array $values Array of bindings to set (null to get the bindings) + * @return array + */ + public function bindings($query, $values = null) { + if(is_null($values)) { + return a::get($this->bindings, $query, array()); + } else { + if(!is_null($query)) $this->bindings[$query] = $values; + + // directly register bindings if possible + if($this->dbquery) $this->dbquery->bindings($values); + } + } + + /** + * Calls an SQL method using the correct database type + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call($method, $arguments) { + $type = $this->database->type(); + + if(isset(static::$methods[$type][$method])) { + $method = static::$methods[$type][$method]; + } else { + // fallback to shared method + if(!isset(static::$methods['_shared'][$method])) { + throw new Error('SQL method ' . $method . ' is not defined for database type ' . $type); + } + + $method = static::$methods['_shared'][$method]; + } + + // pass the sql object as first argument + array_unshift($arguments, $this); + return call($method, $arguments); + } + + /** + * Registers a method for a specified database type + * The function must take this SQL object as first parameter and set bindings on it + * + * @param string $name + * @param callable $function + * @param string $type 'mysql', 'sqlite' or '_shared' + */ + public static function registerMethod($name, $function, $type = '_shared') { + if(!isset(static::$methods[$type])) static::$methods[$type] = array(); + static::$methods[$type][$name] = $function; + } + + /** + * Returns a randomly generated binding name + * + * @param string $label String that contains lowercase letters and numbers to use as a readable identifier + * @return string + */ + public static function generateBindingName($label) { + // make sure that the binding name is valid to prevent injections + if(!preg_match('/^[a-z0-9]+$/', $label)) $label = 'invalid'; + + return ':' . $label . '_' . uniqid(); + } + +} + +/** + * Builds a select clause + * + * @param array $params List of parameters for the select clause. Check out the defaults for more info. + * @return string + */ +sql::registerMethod('select', function($sql, $params = array()) { + + $defaults = array( + 'table' => '', + 'columns' => '*', + 'join' => false, + 'distinct' => false, + 'where' => false, + 'group' => false, + 'having' => false, + 'order' => false, + 'offset' => 0, + 'limit' => false, + ); + + $options = array_merge($defaults, $params); + $query = array(); + $bindings = array(); + + $query[] = 'SELECT'; + + // select distinct values + if($options['distinct']) $query[] = 'DISTINCT'; + + // validate table + if(!$sql->database->validateTable($options['table'])) throw new Error('Invalid table ' . $options['table']); + + // columns + if(empty($options['columns'])) { + $query[] = '*'; + } else if(is_array($options['columns'])) { + // validate columns + $columns = array(); + foreach($options['columns'] as $column) { + list($table, $columnPart) = $sql->splitIdentifier($options['table'], $column); + if(!$sql->database->validateColumn($table, $columnPart)) { + throw new Error('Invalid column ' . $column); + } + + $columns[] = $sql->combineIdentifier($table, $columnPart); + } + + $query[] = implode(', ', $columns); + } else { + $query[] = $options['columns']; + } + + // table + $query[] = 'FROM ' . $sql->quoteIdentifier($options['table']); + + // join + if(!empty($options['join'])) { + foreach($options['join'] as $join) { + $joinType = ltrim(strtoupper(a::get($join, 'type', '')) . ' JOIN'); + if(!in_array($joinType, array( + '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' + ))) throw new Error('Invalid join type ' . $joinType); + + // validate table + if(!$sql->database->validateTable($join['table'])) throw new Error('Invalid table ' . $join['table']); + + // ON can't be escaped here + $query[] = $joinType . ' ' . $sql->quoteIdentifier($join['table']) . ' ON ' . $join['on']; + } + } + + // where + if(!empty($options['where'])) { + // WHERE can't be escaped here + $query[] = 'WHERE ' . $options['where']; + } + + // group + if(!empty($options['group'])) { + // GROUP BY can't be escaped here + $query[] = 'GROUP BY ' . $options['group']; + } + + // having + if(!empty($options['having'])) { + // HAVING can't be escaped here + $query[] = 'HAVING ' . $options['having']; + } + + // order + if(!empty($options['order'])) { + // ORDER BY can't be escaped here + $query[] = 'ORDER BY ' . $options['order']; + } + + // offset and limit + if($options['offset'] > 0 || $options['limit']) { + if(!$options['limit']) $options['limit'] = '18446744073709551615'; + + $offsetBinding = sql::generateBindingName('offset'); + $bindings[$offsetBinding] = $options['offset']; + $limitBinding = sql::generateBindingName('limit'); + $bindings[$limitBinding] = $options['limit']; + + $query[] = 'LIMIT ' . $offsetBinding . ', ' . $limitBinding; + } + + $query = implode(' ', $query); + + $sql->bindings($query, $bindings); + return $query; + +}); + +/** + * Builds an insert clause + * + * @param array $params List of parameters for the insert clause. See defaults for more info. + * @return string + */ +sql::registerMethod('insert', function($sql, $params = array()) { + + $defaults = array( + 'table' => '', + 'values' => false, + ); + + $options = array_merge($defaults, $params); + $query = array(); + $bindings = array(); + + // validate table + if(!$sql->database->validateTable($options['table'])) throw new Error('Invalid table ' . $options['table']); + + $query[] = 'INSERT INTO ' . $sql->quoteIdentifier($options['table']); + $query[] = $sql->values($options['table'], $options['values'], ', ', false); + + $query = implode(' ', $query); + + $sql->bindings($query, $bindings); + return $query; + +}); + +/** + * Builds an update clause + * + * @param array $params List of parameters for the update clause. See defaults for more info. + * @return string + */ +sql::registerMethod('update', function($sql, $params = array()) { + + $defaults = array( + 'table' => '', + 'values' => false, + 'where' => false, + ); + + $options = array_merge($defaults, $params); + $query = array(); + $bindings = array(); + + // validate table + if(!$sql->database->validateTable($options['table'])) throw new Error('Invalid table ' . $options['table']); + + $query[] = 'UPDATE ' . $sql->quoteIdentifier($options['table']) . ' SET'; + $query[] = $sql->values($options['table'], $options['values']); + + if(!empty($options['where'])) { + // WHERE can't be escaped here + $query[] = 'WHERE ' . $options['where']; + } + + $query = implode(' ', $query); + + $sql->bindings($query, $bindings); + return $query; + +}); + +/** + * Builds a delete clause + * + * @param array $params List of parameters for the delete clause. See defaults for more info. + * @return string + */ +sql::registerMethod('delete', function($sql, $params = array()) { + + $defaults = array( + 'table' => '', + 'where' => false, + ); + + $options = array_merge($defaults, $params); + $query = array(); + $bindings = array(); + + // validate table + if(!$sql->database->validateTable($options['table'])) throw new Error('Invalid table ' . $options['table']); + + $query[] = 'DELETE FROM ' . $sql->quoteIdentifier($options['table']); + + if(!empty($options['where'])) { + // WHERE can't be escaped here + $query[] = 'WHERE ' . $options['where']; + } + + $query = implode(' ', $query); + + $sql->bindings($query, $bindings); + return $query; + +}); + +/** + * 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 + * @return string + */ +sql::registerMethod('values', function($sql, $table, $values, $separator = ', ', $set = true, $enforceQualified = false) { + + if(!is_array($values)) return $values; + + if($set) { + + $output = array(); + $bindings = array(); + + foreach($values as $key => $value) { + // validate column + list($table, $column) = $sql->splitIdentifier($table, $key); + if(!$sql->database->validateColumn($table, $column)) { + throw new Error('Invalid column ' . $key); + } + $key = $sql->combineIdentifier($table, $column, $enforceQualified !== true); + + if(in_array($value, sql::$literals, true)) { + $output[] = $key . ' = ' . (($value === null)? 'null' : $value); + continue; + } elseif(is_array($value)) { + $value = json_encode($value); + } + + $valueBinding = sql::generateBindingName('value'); + $bindings[$valueBinding] = $value; + + $output[] = $key . ' = ' . $valueBinding; + } + + $sql->bindings(null, $bindings); + return implode($separator, $output); + + } else { + + $fields = array(); + $output = array(); + $bindings = array(); + + foreach($values as $key => $value) { + // validate column + list($table, $column) = $sql->splitIdentifier($table, $key); + if(!$sql->database->validateColumn($table, $column)) { + throw new Error('Invalid column ' . $key); + } + $key = $sql->combineIdentifier($table, $column, $enforceQualified !== true); + + $fields[] = $key; + + if(in_array($value, sql::$literals, true)) { + $output[] = ($value === null)? 'null' : $value; + continue; + } elseif(is_array($value)) { + $value = json_encode($value); + } + + $valueBinding = sql::generateBindingName('value'); + $bindings[$valueBinding] = $value; + + $output[] = $valueBinding; + } + + $sql->bindings(null, $bindings); + return '(' . implode($separator, $fields) . ') VALUES (' . implode($separator, $output) . ')'; + + } + +}); + +/** + * Creates the sql for dropping a single table + * + * @param string $table + * @return string + */ +sql::registerMethod('dropTable', function($sql, $table) { + + // validate table + if(!$sql->database->validateTable($table)) throw new Error('Invalid table ' . $table); + + return 'DROP TABLE ' . $sql->quoteIdentifier($table); + +}); + +/** + * Creates a table with a simple scheme array for columns + * MySQL version + * + * @todo add more options per column + * @param string $table The table name + * @param array $columns + * @return string + */ +sql::registerMethod('createTable', function($sql, $table, $columns = array()) { + + $output = array(); + $keys = array(); + $bindings = array(); + + foreach($columns as $name => $column) { + // column type + if(!isset($column['type'])) throw new Error('No column type given for column ' . $name); + switch($column['type']) { + case 'id': + $template = '{column.name} INT(11) UNSIGNED NOT NULL AUTO_INCREMENT'; + $column['key'] = 'PRIMARY'; + break; + case 'varchar': + $template = '{column.name} varchar(255) {column.null} {column.default}'; + break; + case 'text': + $template = '{column.name} TEXT'; + break; + case 'int': + $template = '{column.name} INT(11) UNSIGNED {column.null} {column.default}'; + break; + case 'timestamp': + $template = '{column.name} TIMESTAMP {column.null} {column.default}'; + break; + default: + throw new Error('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'])) { + $column['key'] = strtoupper($column['key']); + + // backwards compatibility + if($column['key'] === 'PRIMARY') $column['key'] = 'PRIMARY KEY'; + + if(in_array($column['key'], array('PRIMARY KEY', 'INDEX'))) { + $key = $column['key']; + $keys[$name] = $key; + } + } + + // default value + $defaultBinding = null; + if(isset($column['default'])) { + $defaultBinding = sql::generateBindingName('default'); + $bindings[$defaultBinding] = $column['default']; + } + + $output[] = trim(str::template($template, array( + 'column.name' => $sql->quoteIdentifier($name), + 'column.null' => $null, + 'column.default' => r(!is_null($defaultBinding), 'DEFAULT ' . $defaultBinding), + ))); + + } + + // combine columns + $inner = implode(',' . PHP_EOL, $output); + + // add keys + foreach($keys as $name => $key) { + $inner .= ',' . PHP_EOL . $key . ' (' . $sql->quoteIdentifier($name) . ')'; + } + + // make it a string + $query = 'CREATE TABLE ' . $sql->quoteIdentifier($table) . ' (' . PHP_EOL . $inner . PHP_EOL . ')'; + + $sql->bindings($query, $bindings); + return $query; + +}, 'mysql'); + +/** + * Creates a table with a simple scheme array for columns + * SQLite version + * + * @todo add more options per column + * @param string $table The table name + * @param array $columns + * @return string + */ +sql::registerMethod('createTable', function($sql, $table, $columns = array()) { + + $output = array(); + $keys = array(); + $bindings = array(); + + foreach($columns as $name => $column) { + // column type + if(!isset($column['type'])) throw new Error('No column type given for column ' . $name); + switch($column['type']) { + case 'id': + $template = '{column.name} INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL UNIQUE'; + break; + case 'varchar': + $template = '{column.name} TEXT {column.null} {column.key} {column.default}'; + break; + case 'text': + $template = '{column.name} TEXT {column.null} {column.key} {column.default}'; + break; + case 'int': + $template = '{column.name} INTEGER {column.null} {column.key} {column.default}'; + break; + case 'timestamp': + $template = '{column.name} INTEGER {column.null} {column.key} {column.default}'; + break; + default: + throw new Error('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'])) { + $column['key'] = strtoupper($column['key']); + + // backwards compatibility + if($column['key'] === 'PRIMARY') $column['key'] = 'PRIMARY KEY'; + + if(in_array($column['key'], array('PRIMARY KEY', 'INDEX'))) { + $key = $column['key']; + $keys[$name] = $key; + } + } + + // default value + $default = null; + if(isset($column['default'])) { + // Apparently SQLite doesn't support bindings for default values + $default = "'" . $sql->database->escape($column['default']) . "'"; + } + + $output[] = trim(str::template($template, array( + 'column.name' => $sql->quoteIdentifier($name), + 'column.null' => $null, + 'column.key' => r($key && $key != 'INDEX', $key), + 'column.default' => r(!is_null($default), 'DEFAULT ' . $default), + ))); + + } + + // combine columns + $inner = implode(',' . PHP_EOL, $output); + + // make it a string + $query = 'CREATE TABLE ' . $sql->quoteIdentifier($table) . ' (' . PHP_EOL . $inner . PHP_EOL . ')'; + + // set bindings for our first query + $sql->bindings($query, $bindings); + + // add index keys + foreach($keys as $name => $key) { + if($key != 'INDEX') continue; + + $indexQuery = 'CREATE INDEX ' . $sql->quoteIdentifier($table . '_' . $name) . ' ON ' . $sql->quoteIdentifier($table) . ' (' . $sql->quoteIdentifier($name) . ')'; + $query .= ';' . PHP_EOL . $indexQuery; + } + + return $query; + +}, 'sqlite'); + +/** + * Splits a (qualified) identifier into table and column + * + * @param $table string Default table if the identifier is not qualified + * @param $identifier string + * @return array + */ +sql::registerMethod('splitIdentifier', function($sql, $table, $identifier) { + + // 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, $sql->unquoteIdentifier($parts[0])); + + // qualified identifier + case 2: + return array($sql->unquoteIdentifier($parts[0]), $sql->unquoteIdentifier($parts[1])); + + // every other number is an error + default: + throw new Error('Invalid identifier ' . $identifier); + } + +}); + +/** + * Unquotes an identifier (table *or* column) + * + * @param $identifier string + * @return string + */ +sql::registerMethod('unquoteIdentifier', function($sql, $identifier) { + + // remove quotes around the identifier + if(in_array(str::substr($identifier, 0, 1), array('"', '`'))) $identifier = str::substr($identifier, 1); + if(in_array(str::substr($identifier, -1), array('"', '`'))) $identifier = str::substr($identifier, 0, -1); + + // unescape duplicated quotes + return str_replace(array('""', '``'), array('"', '`'), $identifier); + +}); + +/** + * Combines an identifier (table and column) + * MySQL 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 + */ +sql::registerMethod('combineIdentifier', function($sql, $table, $column, $values = false) { + + return $sql->quoteIdentifier($table) . '.' . $sql->quoteIdentifier($column); + +}, 'mysql'); + +/** + * 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 + */ +sql::registerMethod('combineIdentifier', function($sql, $table, $column, $values = false) { + + // SQLite doesn't support qualified column names for VALUES clauses + if($values) return $sql->quoteIdentifier($column); + return $sql->quoteIdentifier($table) . '.' . $sql->quoteIdentifier($column); + +}, 'sqlite'); + +/** + * Quotes an identifier (table *or* column) + * MySQL version + * + * @param $identifier string + * @return string + */ +sql::registerMethod('quoteIdentifier', function($sql, $identifier) { + + // * is special + if($identifier === '*') return $identifier; + + // replace every backtick with two backticks + $identifier = str_replace('`', '``', $identifier); + + // wrap in backticks + return '`' . $identifier . '`'; + +}, 'mysql'); + +/** + * Quotes an identifier (table *or* column) + * SQLite version + * + * @param $identifier string + * @return string + */ +sql::registerMethod('quoteIdentifier', function($sql, $identifier) { + + // * is special + if($identifier === '*') return $identifier; + + // replace every quote with two quotes + $identifier = str_replace('"', '""', $identifier); + + // wrap in quotes + return '"' . $identifier . '"'; + +}, 'sqlite'); + +/** + * Returns a list of tables for a specified database + * MySQL version + * + * @param string $database The database name + * @return string + */ +sql::registerMethod('tableList', function($sql, $database) { + + $bindings = array(); + $databaseBinding = sql::generateBindingName('database'); + $bindings[$databaseBinding] = $database; + + $query = 'SELECT TABLE_NAME AS name FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ' . $databaseBinding; + + $sql->bindings($query, $bindings); + return $query; + +}, 'mysql'); + +/** + * Returns a list of tables of the database + * SQLite version + * + * @param string $database The database name + * @return string + */ +sql::registerMethod('tableList', function($sql, $database) { + + return 'SELECT name FROM sqlite_master WHERE type = "table"'; + +}, 'sqlite'); + +/** + * Returns a list of columns for a specified table + * MySQL version + * + * @param string $database The database name + * @param string $table The table name + * @return string + */ +sql::registerMethod('columnList', function($sql, $database, $table) { + + $bindings = array(); + $databaseBinding = sql::generateBindingName('database'); + $bindings[$databaseBinding] = $database; + $tableBinding = sql::generateBindingName('table'); + $bindings[$tableBinding] = $table; + + $query = 'SELECT COLUMN_NAME AS name FROM INFORMATION_SCHEMA.COLUMNS '; + $query .= 'WHERE TABLE_SCHEMA = ' . $databaseBinding . ' AND TABLE_NAME = ' . $tableBinding; + + $sql->bindings($query, $bindings); + return $query; + +}, 'mysql'); + +/** + * Returns a list of columns for a specified table + * SQLite version + * + * @param string $database The database name + * @param string $table The table name + * @return string + */ +sql::registerMethod('columnList', function($sql, $database, $table) { + + // validate table + if(!$sql->database->validateTable($table)) throw new Error('Invalid table ' . $table); + + return 'PRAGMA table_info(' . $sql->quoteIdentifier($table) . ')'; + +}, 'sqlite'); diff --git a/kirby/toolkit/lib/str.php b/kirby/toolkit/lib/str.php new file mode 100644 index 0000000..540c59e --- /dev/null +++ b/kirby/toolkit/lib/str.php @@ -0,0 +1,701 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Str { + + public static $ascii = array( + '/Ä/' => 'Ae', + '/æ|ǽ|ä/' => 'ae', + '/œ|ö/' => 'oe', + '/À|Á|Â|Ã|Å|Ǻ|Ā|Ă|Ą|Ǎ|А/' => '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', + '/Ö/' => 'Oe', + '/ö/' => 'oe', + '/Ò|Ó|Ô|Õ|Ō|Ŏ|Ǒ|Ő|Ơ|Ø|Ǿ|О/' => 'O', + '/ò|ó|ô|õ|ō|ŏ|ǒ|ő|ơ|ø|ǿ|º|о/' => 'o', + '/П/' => 'P', + '/п/' => 'p', + '/Ŕ|Ŗ|Ř|Р/' => 'R', + '/ŕ|ŗ|ř|р/' => 'r', + '/Ś|Ŝ|Ş|Ș|Š|С/' => 'S', + '/ś|ŝ|ş|ș|š|ſ|с/' => 's', + '/Ţ|Ț|Ť|Ŧ|Т/' => 'T', + '/ţ|ț|ť|ŧ|т/' => 't', + '/Ü/' => 'Ue', + '/ü/' => 'ue', + '/Ù|Ú|Û|Ũ|Ū|Ŭ|Ů|Ű|Ų|Ư|Ǔ|Ǖ|Ǘ|Ǚ|Ǜ|У/' => '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 options for string methods + * + * @var array + */ + public static $defaults = array( + 'slug' => array( + 'separator' => '-', + 'allowed' => 'a-z0-9' + ) + ); + + /** + * Converts a string to a html-safe string + * + * + * + * echo str::html('some über crazy stuff'); + * // output: some über crazy stuff + * + * echo str::html('some über crazy stuff', false); + * // output: some <em>über crazy</em> stuff + * + * + * + * @param string $string + * @param boolean $keepTags True: lets stuff inside html tags untouched. + * @return string The html string + */ + public static function html($string, $keepTags = true) { + return html::encode($string, $keepTags); + } + + /** + * 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) { + 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 str::xml('some über crazy stuff'); + * // output: some über crazy stuff + * + * + * + * @param string $text + * @param boolean $html True: convert to html first + * @return string + */ + public static function xml($text, $html = true) { + return xml::encode($text, $html); + } + + /** + * Removes all xml entities from a string + * and convert them to html entities first + * and remove all html entities afterwards. + * + * + * + * echo str::unxml('some über crazy stuff'); + * // output: some über crazy stuff + * + * + * + * @param string $string + * @return string + */ + public static function unxml($string) { + return xml::decode($string); + } + + /** + * Parses a string by a set of available methods + * + * Available methods: + * - json + * - xml + * - url + * - query + * - php + * + * + * + * str::parse('{"test":"cool","super":"genious"}'); + * // output: array( + * // 'test' => 'cool', + * // 'super' => 'genious' + * // ); + * + * str::parse('nice', 'xml'); + * // output: array( + * // 'entries' => array( + * // 'cool' => 'nice' + * // ) + * // ); + * + * + * + * @param string $string + * @param string $mode + * @return mixed + */ + public static function parse($string, $mode = 'json') { + + if(is_array($string) || is_object($string)) return $string; + + switch($mode) { + case 'json': + return (array)@json_decode($string, true); + case 'xml': + return xml::parse($string); + case 'url': + return (array)@parse_url($string); + case 'php': + return @unserialize($string); + default: + return $string; + } + + } + + /** + * Encode a string (used for email addresses) + * + * @param string $string + * @return string + */ + public static function encode($string) { + $string = (string)$string; + $encoded = ''; + for($i = 0; $i < static::length($string); $i++) { + $char = static::substr($string, $i, 1); + if(MB) { + list(, $code) = unpack('N', mb_convert_encoding($char, 'UCS-4BE', 'UTF-8')); + } else { + $code = ord($char); + } + + $encoded .= rand(1, 2) == 1 ? '&#' . $code . ';' : '&#x' . dechex($code) . ';'; + } + return $encoded; + } + + /** + * Generates an "a mailto" tag + * + * + * + * echo str::email('bastian@getkirby.com'); + * echo str::email('bastian@getkirby.com', 'mail me'); + * + * + * + * @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($email, $text = false, $attr = array()) { + return html::email($email, $text, $attr); + } + + /** + * 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 link($href, $text = null, $attr = array()) { + return html::a($href, $text, $attr); + } + + /** + * Returns an array with all words in a string + * + * @param string $string + */ + public static function words($string) { + preg_match_all('/(\pL{4,})/iu', $string, $m); + return array_shift($m); + } + + /** + * Returns an array with all sentences in a string + * + * @param string $string + * @return string + */ + public static function sentences($string) { + return preg_split('/(?<=[.?!])\s+/', $string, -1, PREG_SPLIT_NO_EMPTY); + } + + /** + * Returns an array with all lines in a string + * + * @param string $string + * @return array + */ + public static function lines($string) { + return str::split($string, PHP_EOL); + } + + /** + * Checks if the given string is a URL + * + * @param string $string + * @return boolean + */ + public static function isURL($string) { + return filter_var($string, FILTER_VALIDATE_URL); + } + + /** + * 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 $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 short($string, $length, $rep = '…') { + if(!$length) return $string; + if(static::length($string) <= $length) return $string; + $string = static::substr($string, 0, $length); + return $string . $rep; + } + + /** + * Creates an excerpt of a string + * It removes all html tags first and then uses str::short + * + * @param string $string The string to be shortened + * @param int $chars The final number of characters the string should have + * @param boolean $removehtml 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, $removehtml = true, $rep='…') { + if($removehtml) $string = strip_tags($string); + $string = str_replace(PHP_EOL, ' ', trim($string)); + if(static::length($string) <= $chars) return $string; + return $chars == 0 ? $string : static::substr($string, 0, strrpos(static::substr($string, 0, $chars), ' ')) . $rep; + } + + /** + * 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 = '') { + return preg_replace_callback('|([^\s])\s+([^\s]+)\s*$|', function($matches) { + if(str::contains($matches[2], '-')) { + return $matches[1] . ' ' . str_replace('-', '‑', $matches[2]); + } else { + return $matches[1] . ' ' . $matches[2]; + } + }, $string); + } + + /** + * An UTF-8 safe version of substr() + * + * @param string $str + * @param int $start + * @param int $length + * @return string + */ + public static function substr($str, $start, $length = null) { + $length = $length === null ? static::length($str) : $length; + return MB ? mb_substr($str, $start, $length, 'UTF-8') : substr($str, $start, $length); + } + + /** + * An UTF-8 safe version of strtolower() + * + * @param string $str + * @return string + */ + public static function lower($str) { + return MB ? mb_strtolower($str, 'UTF-8') : strtolower($str); + } + + /** + * An UTF-8 safe version of strotoupper() + * + * @param string $str + * @return string + */ + public static function upper($str) { + return MB ? mb_strtoupper($str, 'UTF-8') : strtoupper($str); + } + + /** + * An UTF-8 safe version of strlen() + * + * @param string $str + * @return string + */ + public static function length($str) { + return MB ? mb_strlen($str, 'UTF-8') : strlen($str); + } + + /** + * Checks if a str contains another string + * + * @param string $str + * @param string $needle + * @param boolean $i ignore upper/lowercase + * @return string + */ + public static function contains($str, $needle, $i = true) { + if($i) { + $str = static::lower($str); + $needle = static::lower($needle); + } + return strstr($str, $needle) ? true : false; + } + + /** + * Generates a random string + * + * @param int $length The length of the random string + * @return string + */ + public static function random($length = false, $type = 'alphaNum') { + $length = $length ? $length : rand(5,10); + $pool = static::pool($type); + shuffle($pool); + $size = count($pool) - 1; + $hash = ''; + for($x = 0; $x < $length; $x++) { + $hash .= $pool[rand(0, $size)]; + } + return $hash; + } + + /** + * 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. + * @return string The safe string + */ + public static function slug($string, $separator = null, $allowed = null) { + + $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); + // remove double dashes + $string = preg_replace('![' . preg_quote($separator) . ']{2,}!', $separator, $string); + // trim trailing and leading dashes + $string = trim($string, $separator); + // replace slashes with dashes + $string = str_replace('/', $separator, $string); + + return $string; + + } + + /** + * 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, $separator = ',', $length = 1) { + + if(is_array($string)) return $string; + + $string = trim($string, $separator); + $parts = explode($separator, $string); + $out = array(); + + foreach($parts AS $p) { + $p = trim($p); + if(static::length($p) > 0 && static::length($p) >= $length) $out[] = $p; + } + + return $out; + + } + + /** + * An UTF-8 safe version of ucwords() + * + * @param string $string + * @return string + */ + public static function ucwords($string) { + return MB ? mb_convert_case($string, MB_CASE_TITLE, 'UTF-8') : ucwords(strtolower($string)); + } + + /** + * An UTF-8 safe version of ucfirst() + * + * @param string $string + * @return string + */ + public static function ucfirst($string) { + return static::upper(static::substr($string, 0, 1)) . static::lower(static::substr($string, 1)); + } + + /** + * Tries to detect the string encoding + * + * @param string $string + * @return string + */ + public static function encoding($string) { + + if(MB) { + return mb_detect_encoding($string, 'UTF-8, ISO-8859-1, windows-1251'); + } else { + foreach(array('utf-8', 'iso-8859-1', 'windows-1251') as $item) { + if(md5(iconv($item, $item, $string)) == md5($string)) return $item; + } + return 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(is_null($sourceEncoding)) $sourceEncoding = static::encoding($string); + return iconv($sourceEncoding, $targetEncoding, $string); + } + + /** + * Converts a string to UTF-8 + * + * @param string $string + * @return string + */ + public static function utf8($string) { + return static::convert($string, 'utf-8'); + } + + /** + * A better way to strip slashes + * + * @param string $string + * @return string + */ + public static function stripslashes($string) { + if(is_array($string)) return $string; + return get_magic_quotes_gpc() ? stripslashes($string) : $string; + } + + /** + * A super simple string template engine, + * which replaces tags like {mytag} with any other string + * + * @param string $string + * @param array $data An associative array with keys, which should be replaced and values. + * @return string + */ + public static function template($string, $data = array()) { + $replace = array(); + foreach($data as $key => $value) $replace['{' . $key . '}'] = $value; + return str_replace(array_keys($replace), array_values($replace), $string); + } + + /** + * Convert a string to 7-bit ASCII. + * + * @param string $string + * @return string + */ + public static function ascii($string) { + $foreign = static::$ascii; + $string = preg_replace(array_keys($foreign), array_values($foreign), $string); + return preg_replace('/[^\x09\x0A\x0D\x20-\x7E]/', '', $string); + } + + /** + * Forces a download of the string as text file + * + * @param string $string + * @param string $name Optional name for the downloaded file + */ + public static function download($string, $name = null) { + + header::download(array( + 'name' => $name ? $name : 'text.txt', + 'size' => static::length($string), + 'mime' => 'text/plain', + )); + + die($string); + + } + + /** + * Checks if a string starts with the passed needle + * + * @param string $string + * @param string $needle + * @return boolean + */ + public static function startsWith($string, $needle) { + return $needle === '' || strpos($string, $needle) === 0; + } + + /** + * Checks if a string ends with the passed needle + * + * @param string $string + * @param string $needle + * @return boolean + */ + public static function endsWith($string, $needle) { + return $needle === '' || static::substr($string, -static::length($needle)) === $needle; + } + + /** + * Get a character pool with various possible combinations + * + * @param string $type + * @param boolean $array + * @return string + */ + public static function pool($type, $array = true) { + + $pool = array(); + + if(is_array($type)) { + foreach($type as $t) { + $pool = array_merge($pool, static::pool($t)); + } + } else { + + switch($type) { + case 'alphaLower': + $pool = range('a','z'); + break; + case 'alphaUpper': + $pool = range('A', 'Z'); + break; + case 'alpha': + $pool = static::pool(array('alphaLower', 'alphaUpper')); + break; + case 'num': + $pool = range(0, 9); + break; + case 'alphaNum': + $pool = static::pool(array('alpha', 'num')); + break; + } + + } + + return $array ? $pool : implode('', $pool); + + } + +} diff --git a/kirby/toolkit/lib/system.php b/kirby/toolkit/lib/system.php new file mode 100644 index 0000000..929d27d --- /dev/null +++ b/kirby/toolkit/lib/system.php @@ -0,0 +1,150 @@ + + * @link http://getkirby.com + * @copyright Lukas Bestle + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class System { + + /** + * Checks if the system() function is available + * + * @return boolean + */ + public static function available() { + return (!ini_get('safe_mode') && function_exists('exec')); + } + + /** + * Checks if a command is executable + * + * @param string $command Name or path of the command to check + * @return boolean + */ + public static function isExecutable($command) { + // check if everything we need is available + if(!static::available()) { + throw new Exception('The exec() function is not available on this system. Probably, safe_mode is on (shame!).'); + } + + // only use the actual command + list($command) = explode(' ', $command); + + // get the path to the executable and check if it exists + $path = static::realpath($command); + return $path !== false; + } + + /** + * Returns the path to a specific executable + * + * @param string $command Name or path of the command + * @return mixed + */ + public static function realpath($command) { + // check if everything we need is available + if(!static::available()) { + throw new Exception('The exec() function is not available on this system. Probably, safe_mode is on (shame!).'); + } + + // if this is actually a file, we don't need to search for it any longer + if(file_exists($command)) { + return is_executable($command) ? realpath($command) : false; + } + + // let the shell search for it + // depends on the operating system + if(strtolower(substr(PHP_OS, 0, 3)) === 'win') { + // Windows + // run the "where" command + $result = `where $command`; + // everything besides "Could not find files" would be OK + $exists = !preg_match('/Could not find files/', $result); + } else { + // Unix + // run the "which" command + $result = `which $command`; + // an empty output means there is no path + $exists = !empty($result); + } + + return $exists ? trim($result) : false; + + } + + /** + * Execute a given shell command + * + * @param string $command Name or path of the command + * @param string $arguments Additional arguments + * @param string $what What to return ('status', 'success', 'output' or 'all') + * @return mixed + */ + public static function execute($command, $arguments = array(), $what = 'all') { + // check if everything we need is available + if(!static::available()) { + throw new Exception('The exec() function is not available on this system. Probably, safe_mode is on (shame!).'); + } + + // other ways of calling this method + if(is_array($command)) { + // everything is given as one array + $what = (is_array($arguments))? 'all' : $arguments; + $arguments = array_slice($command, 1); + $command = $command[0]; + } else if(!is_array($arguments)) { + // each additional argument is given as a new method argument + $arguments = array_slice(func_get_args(), 1); + $what = 'all'; + } + + // check if the command exists + if(!static::isExecutable($command)) { + throw new Exception('The command "' . $command . '" is not executable.'); + } + + // escape command + $command = escapeshellcmd($command); + + // escape arguments + array_walk($arguments, function(&$argument) { + $argument = escapeshellarg($argument); + }); + + // execute the command + exec($command . ' ' . implode(' ', $arguments) . ' 2>&1', $output, $status); + + $result = array( + 'output' => implode("\n", $output), + 'status' => $status, + 'success' => $status === 0 + ); + + // return an appropriate result + if($what === 'all' || !array_key_exists($what, $result)) { + return $result; + } else { + return $result[$what]; + } + } + + /** + * Execute a given shell command + * Alias for System::execute() + * + * @param string $command Name or path of the command + * @param string $arguments Additional arguments + * @return array + */ + public static function __callStatic($command, $arguments) { + return static::execute($command, $arguments, 'all'); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/thumb.php b/kirby/toolkit/lib/thumb.php new file mode 100644 index 0000000..bc8b534 --- /dev/null +++ b/kirby/toolkit/lib/thumb.php @@ -0,0 +1,358 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Thumb extends Obj { + + const ERROR_INVALID_IMAGE = 0; + const ERROR_INVALID_DRIVER = 1; + + public static $drivers = array(); + + public static $defaults = array( + 'destination' => false, + 'filename' => '{safeName}-{hash}.{extension}', + 'url' => '/thumbs', + 'root' => '/thumbs', + 'driver' => 'im', + 'memory' => '128M', + 'quality' => 90, + 'blur' => false, + 'blurpx' => 10, + 'width' => null, + 'height' => null, + 'upscale' => false, + 'crop' => false, + 'grayscale' => false, + 'overwrite' => false, + 'autoOrient' => false, + 'interlace' => false + ); + + public $source = null; + public $result = null; + public $destination = null; + public $options = array(); + public $error = null; + + /** + * Constructor + * + * @param mixed $source + * @param array $params + */ + public function __construct($source, $params = array()) { + + $this->source = $this->result = is_a($source, 'Media') ? $source : new Media($source); + $this->options = array_merge(static::$defaults, $this->params($params)); + $this->destination = $this->destination(); + + // don't create the thumbnail if it's not necessary + if($this->isObsolete()) return; + + // don't create the thumbnail if it exists + if(!$this->isThere()) { + + // try to create the thumb folder if it is not there yet + dir::make(dirname($this->destination->root)); + + // check for a valid image + if(!$this->source->exists() || $this->source->type() != 'image') { + throw new Error('The given image is invalid', static::ERROR_INVALID_IMAGE); + } + + // check for a valid driver + if(!array_key_exists($this->options['driver'], static::$drivers)) { + throw new Error('Invalid thumbnail driver', static::ERROR_INVALID_DRIVER); + } + + // create the thumbnail + $this->create(); + + // check if creating the thumbnail failed + if(!file_exists($this->destination->root)) return; + + } + + // create the result object + $this->result = new Media($this->destination->root, $this->destination->url); + + } + + /** + * Build the destination object + * + * @return Obj + */ + public function destination() { + + if(is_callable($this->options['destination'])) { + return call($this->options['destination'], $this); + } else { + + $destination = new Obj(); + $safeName = f::safeName($this->source->name()); + + $destination->filename = str::template($this->options['filename'], array( + 'extension' => $this->source->extension(), + 'name' => $this->source->name(), + 'filename' => $this->source->filename(), + 'safeName' => $safeName, + 'safeFilename' => $safeName . '.' . $this->extension(), + 'width' => $this->options['width'], + 'height' => $this->options['height'], + 'hash' => md5($this->source->root() . $this->settingsIdentifier()), + )); + + $destination->url = $this->options['url'] . '/' . $destination->filename; + $destination->root = $this->options['root'] . DS . $destination->filename; + + return $destination; + + } + + } + + /** + * Returns the source media object + * + * @return Media + */ + public function source() { + return $this->source; + } + + /** + * Returns the exception if available + * + * @return Exception + */ + public function error() { + return $this->error; + } + + /** + * Makes it possible to pass a string of params + * which is shorter and more convenient than + * passing a full array of keys and values: + * width:300|height:200|crop:true + * + * @param array $params + * @return array + */ + public function params($params) { + if(is_array($params)) return $params; + $result = array(); + foreach(explode('|', $params) as $param) { + $pos = strpos($param, ':'); + $result[trim(substr($param, 0, $pos))] = trim(substr($param, $pos+1)); + } + return $result; + } + + /** + * Builds a hash for all relevant settings + * + * @return string + */ + public function settingsIdentifier() { + + // build the settings string + return implode('-', array( + ($this->options['width']) ? $this->options['width'] : 0, + ($this->options['height']) ? $this->options['height'] : 0, + ($this->options['upscale']) ? $this->options['upscale'] : 0, + ($this->options['crop']) ? $this->options['crop'] : 0, + $this->options['blur'], + $this->options['grayscale'], + $this->options['quality'] + )); + + } + + /** + * Checks if the thumbnail already exists + * and is newer than the original file + * + * @return boolean + */ + public function isThere() { + + if($this->options['overwrite'] === true) return false; + + // if the thumb already exists and the source hasn't been updated + // we don't need to generate a new thumbnail + if(file_exists($this->destination->root) && f::modified($this->destination->root) >= $this->source->modified()) return true; + + return false; + + } + + /** + * Checks if the thumbnail is not needed + * because the original image is small enough + * + * @return boolean + */ + public function isObsolete() { + + if($this->options['overwrite'] === true) return false; + + // try to use the original if resizing is not necessary + if($this->options['width'] >= $this->source->width() && + $this->options['height'] >= $this->source->height() && + $this->options['crop'] == false && + $this->options['blur'] == false && + $this->options['upscale'] == false) return true; + + return false; + + } + + /** + * Calls the driver function and + * creates the thumbnail + */ + protected function create() { + return call_user_func_array(static::$drivers[$this->options['driver']], array($this)); + } + + /** + * Makes all public methods of the result object + * available to the thumb class + * + * @param string $method + * @param mixed $arguments + * @return mixed + */ + public function __call($method, $arguments) { + + if(method_exists($this->result, $method)) { + return call_user_func_array(array($this->result, $method), $arguments); + } + + } + + /** + * Generates and returns the full html tag for the thumbnail + * + * @param array $attr An optional array of attributes, which should be added to the image tag + * @return string + */ + public function tag($attr = array()) { + + // don't return the tag if the url is not available + if(!$this->result->url()) return false; + + return html::img($this->result->url(), array_merge(array( + 'alt' => isset($this->options['alt']) ? $this->options['alt'] : ' ', + 'class' => isset($this->options['class']) ? $this->options['class'] : null, + ), $attr)); + + } + + /** + * Makes it possible to echo the entire object + */ + public function __toString() { + return $this->tag(); + } + +} + + + +/** + * ImageMagick Driver + */ +thumb::$drivers['im'] = function($thumb) { + + $command = array(); + + $command[] = isset($thumb->options['bin']) ? $thumb->options['bin'] : 'convert'; + $command[] = '"' . $thumb->source->root() . '"'; + $command[] = '-strip'; + + if($thumb->options['interlace']) { + $command[] = '-interlace line'; + } + + if($thumb->source->extension() === 'gif') { + $command[] = '-coalesce'; + } + + if($thumb->options['grayscale']) { + $command[] = '-colorspace gray'; + } + + if($thumb->options['autoOrient']) { + $command[] = '-auto-orient'; + } + + $command[] = '-resize'; + + if($thumb->options['crop']) { + $command[] = $thumb->options['width'] . 'x' . $thumb->options['height'] . '^'; + $command[] = '-gravity Center -crop ' . $thumb->options['width'] . 'x' . $thumb->options['height'] . '+0+0'; + } else { + $dimensions = clone $thumb->source->dimensions(); + $dimensions->fitWidthAndHeight($thumb->options['width'], $thumb->options['height'], $thumb->options['upscale']); + $command[] = $dimensions->width() . 'x' . $dimensions->height() . '!'; + } + + $command[] = '-quality ' . $thumb->options['quality']; + + if($thumb->options['blur']) { + $command[] = '-blur 0x' . $thumb->options['blurpx']; + } + + $command[] = '-limit thread 1'; + $command[] = '"' . $thumb->destination->root . '"'; + + exec(implode(' ', $command)); + +}; + + +/** + * GDLib Driver + */ +thumb::$drivers['gd'] = function($thumb) { + + try { + $img = new abeautifulsite\SimpleImage($thumb->root()); + $img->quality = $thumb->options['quality']; + + if($thumb->options['crop']) { + @$img->thumbnail($thumb->options['width'], $thumb->options['height']); + } else { + $dimensions = clone $thumb->source->dimensions(); + $dimensions->fitWidthAndHeight($thumb->options['width'], $thumb->options['height'], $thumb->options['upscale']); + @$img->resize($dimensions->width(), $dimensions->height()); + } + + if($thumb->options['grayscale']) { + $img->desaturate(); + } + + if($thumb->options['blur']) { + $img->blur('gaussian', $thumb->options['blurpx']); + } + + if($thumb->options['autoOrient']) { + $img->auto_orient(); + } + + @$img->save($thumb->destination->root); + } catch(Exception $e) { + $thumb->error = $e; + } + +}; diff --git a/kirby/toolkit/lib/timer.php b/kirby/toolkit/lib/timer.php new file mode 100644 index 0000000..0325205 --- /dev/null +++ b/kirby/toolkit/lib/timer.php @@ -0,0 +1,28 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Timer { + + public static $time = null; + + public static function start() { + $time = explode(' ', microtime()); + static::$time = (double)$time[1] + (double)$time[0]; + } + + public static function stop() { + $time = explode(' ', microtime()); + $time = (double)$time[1] + (double)$time[0]; + $timer = static::$time; + return round(($time-$timer), 5); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/toolkit.php b/kirby/toolkit/lib/toolkit.php new file mode 100644 index 0000000..2f2a77e --- /dev/null +++ b/kirby/toolkit/lib/toolkit.php @@ -0,0 +1,20 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Toolkit { + + public static $version = '2.3.0'; + + public static function version() { + return static::$version; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/tpl.php b/kirby/toolkit/lib/tpl.php new file mode 100644 index 0000000..cc06212 --- /dev/null +++ b/kirby/toolkit/lib/tpl.php @@ -0,0 +1,29 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Tpl extends Silo { + + public static $data = array(); + + public static function load($_file, $_data = array(), $_return = true) { + if(!file_exists($_file)) return false; + ob_start(); + extract(array_merge(static::$data, (array)$_data)); + require($_file); + $_content = ob_get_contents(); + ob_end_clean(); + if($_return) return $_content; + echo $_content; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/upload.php b/kirby/toolkit/lib/upload.php new file mode 100644 index 0000000..6a306e0 --- /dev/null +++ b/kirby/toolkit/lib/upload.php @@ -0,0 +1,202 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Upload { + + const ERROR_FAILED_UPLOAD = 0; + const ERROR_MISSING_TMP_DIR = 1; + const ERROR_MISSING_FILE = 2; + const ERROR_UNALLOWED_OVERWRITE = 3; + const ERROR_PARTIAL_UPLOAD = 4; + const ERROR_MAX_SIZE = 5; + const ERROR_MOVE_FAILED = 6; + const ERROR_UNACCEPTED = 7; + + public $options = array(); + public $error = null; + public $file = null; + public $to = null; + + public function __construct($to, $params = array()) { + + $defaults = array( + 'input' => 'file', + 'index' => 0, + 'to' => $to, + 'overwrite' => true, + 'maxSize' => false, + 'accept' => null, + ); + + $this->options = array_merge($defaults, $params); + + try { + $this->move(); + $this->file = new Media($this->to()); + } catch(Exception $e) { + $this->error = $e; + } + + } + + public function error() { + return $this->error; + } + + public function source() { + + $source = isset($_FILES[$this->options['input']]) ? $_FILES[$this->options['input']] : null; + + // get the correct file out of multiple based on the "index" option + if($source && is_int($this->options['index']) && is_array($source['name'])) { + $allSources = $source; + $source = array(); + + // get the correct value out of the $values array with all files + foreach($allSources as $key => $values) { + $source[$key] = isset($values[$this->options['index']]) ? $values[$this->options['index']] : null; + } + } + + // prevent duplicate ios uploads + // ios automatically uploads all images as image.jpg, + // which will lead to overwritten duplicates. + // this dirty hack will simply add a uniqid between the + // name and the extension to avoid duplicates + if($source && f::name($source['name']) == 'image' && detect::ios()) { + $source['name'] = 'image-' . uniqid() . '.' . ltrim(f::extension($source['name']), '.'); + } + + return $source; + + } + + public function to() { + + if(!is_null($this->to)) return $this->to; + + $source = $this->source(); + $name = f::name($source['name']); + $extension = f::extension($source['name']); + $safeName = f::safeName($name); + $safeExtension = str_replace('jpeg', 'jpg', str::lower($extension)); + + if(empty($safeExtension)) { + $safeExtension = f::mimeToExtension(f::mime($source['tmp_name'])); + } + + return $this->to = str::template($this->options['to'], array( + 'name' => $name, + 'filename' => $source['name'], + 'safeName' => $safeName, + 'safeFilename' => $safeName . r(!empty($safeExtension), '.' . $safeExtension), + 'extension' => $extension, + 'safeExtension' => $safeExtension + )); + + } + + /** + * Returns the maximum accepted file size + * + * @return int + */ + public function maxSize() { + $sizes = array(detect::maxPostSize(), detect::maxUploadSize()); + if($this->options['maxSize']) { + $sizes[] = $this->options['maxSize']; + } + return min($sizes); + } + + public function file() { + return $this->file; + } + + protected function move() { + + $source = $this->source(); + + if(is_null($source['name']) || is_null($source['tmp_name'])) { + $this->fail(static::ERROR_MISSING_FILE); + } + + if($source['error'] !== 0) { + + switch($source['error']) { + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + $this->fail(static::ERROR_MAX_SIZE); + case UPLOAD_ERR_PARTIAL: + $this->fail(static::ERROR_PARTIAL_UPLOAD); + case UPLOAD_ERR_NO_FILE: + $this->fail(static::ERROR_MISSING_FILE); + case UPLOAD_ERR_NO_TMP_DIR: + $this->fail(static::ERROR_MISSING_TMP_DIR); + case UPLOAD_ERR_CANT_WRITE: + $this->fail(static::ERROR_MOVE_FAILED); + case UPLOAD_ERR_EXTENSION: + $this->fail(static::ERROR_UNACCEPTED); + default: + $this->fail(static::ERROR_FAILED_UPLOAD); + } + + } + + if(file_exists($this->to()) && $this->options['overwrite'] === false) { + $this->fail(static::ERROR_UNALLOWED_OVERWRITE); + } + + if($this->options['maxSize'] && $source['size'] > $this->options['maxSize']) { + $this->fail(static::ERROR_MAX_SIZE); + } + + if(is_callable($this->options['accept'])) { + $accepted = call($this->options['accept'], new Media($source['tmp_name'])); + if($accepted === false) { + $this->fail(static::ERROR_UNACCEPTED); + } + } + + if(!@move_uploaded_file($source['tmp_name'], $this->to())) { + $this->fail(static::ERROR_MOVE_FAILED); + } + + } + + protected function messages() { + return array( + static::ERROR_MISSING_FILE => 'The file is missing', + static::ERROR_MISSING_TMP_DIR => 'The /tmp directory is missing on your server', + static::ERROR_FAILED_UPLOAD => 'The upload failed', + static::ERROR_PARTIAL_UPLOAD => 'The file has been only been partially uploaded', + static::ERROR_UNALLOWED_OVERWRITE => 'The file exists and cannot be overwritten', + static::ERROR_MAX_SIZE => 'The file is too big. The maximum size is ' . f::niceSize($this->maxSize()), + static::ERROR_MOVE_FAILED => 'The file could not be moved', + static::ERROR_UNACCEPTED => 'The file is not accepted by the server' + ); + } + + protected function fail($code) { + + $messages = $this->messages(); + + if(!isset($messages[$code])) { + $code = static::ERROR_FAILED_UPLOAD; + } + + throw new Error($messages[$code], $code); + + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/url.php b/kirby/toolkit/lib/url.php new file mode 100644 index 0000000..bedc9c6 --- /dev/null +++ b/kirby/toolkit/lib/url.php @@ -0,0 +1,391 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Url { + + public static $home = '/'; + public static $to = null; + public static $current = null; + + public static function scheme($url = null) { + if(is_null($url)) { + if( + (isset($_SERVER['HTTPS']) && !empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off') || + server::get('SERVER_PORT') == '443' || + server::get('HTTP_X_FORWARDED_PORT') == '443' || + server::get('HTTP_X_FORWARDED_PROTO') == 'https' || + server::get('HTTP_X_FORWARDED_PROTO') == 'https, http' + ) { + return 'https'; + } else { + return 'http'; + } + } + return parse_url($url, PHP_URL_SCHEME); + + } + + /** + * Returns the current url with all bells and whistles + * + * @return string + */ + public static function current() { + if(!is_null(static::$current)) return static::$current; + return static::$current = static::base() . server::get('REQUEST_URI'); + } + + /** + * Returns the url for the current directory + * + * @return string + */ + public static function currentDir() { + return dirname(static::current()); + } + + /** + */ + public static function host($url = null) { + if(is_null($url)) $url = static::current(); + return parse_url($url, PHP_URL_HOST); + } + + /** + * Returns the port for the given url + * + * @return mixed + */ + public static function port($url = null) { + if(is_null($url)) $url = static::current(); + $port = intval(parse_url($url, PHP_URL_PORT)); + return v::between($port, 1, 65535) ? $port : false; + } + + /** + * Returns only the cleaned path of the url + */ + public static function path($url = null) { + + if(is_null($url)) $url = static::current(); + + // if a path is passed, let's pretend this is an absolute url + // to trick the url parser. It's a bit hacky but it works + if(!static::isAbsolute($url)) $url = 'http://0.0.0.0/' . $url; + + return trim(parse_url($url, PHP_URL_PATH), '/'); + + } + + /** + * Returns the correct separator for parameters + * depending on the operating system + * + * @return string + */ + public static function paramSeparator() { + return detect::windows() ? ';' : ':'; + } + + /** + * Returns the params in the url + */ + public static function params($url = null) { + if(is_null($url)) $url = static::current(); + $path = static::path($url); + if(empty($path)) return array(); + $params = array(); + foreach(explode('/', $path) as $part) { + $pos = strpos($part, static::paramSeparator()); + if($pos === false) continue; + $params[substr($part, 0, $pos)] = urldecode(substr($part, $pos+1)); + } + return $params; + } + + /** + * Returns the path without params + */ + public static function fragments($url = null) { + if(is_null($url)) $url = static::current(); + $path = static::path($url); + if(empty($path)) return null; + $frag = array(); + foreach(explode('/', $path) as $part) { + if(strpos($part, static::paramSeparator()) === false) $frag[] = $part; + } + return $frag; + } + + /** + * Returns the query as array + */ + public static function query($url = null) { + if(is_null($url)) $url = static::current(); + parse_str(parse_url($url, PHP_URL_QUERY), $array); + return $array; + } + + /** + * Checks if the url contains a query string + */ + public static function hasQuery($url = null) { + if(is_null($url)) $url = static::current(); + return str::contains($url, '?'); + } + + /** + */ + public static function hash($url = null) { + if(is_null($url)) $url = static::current(); + return parse_url($url, PHP_URL_FRAGMENT); + } + + public static function build($parts = array(), $url = null) { + + if(is_null($url)) $url = static::current(); + + $defaults = array( + 'scheme' => static::scheme($url), + 'host' => static::host($url), + 'port' => static::port($url), + 'fragments' => static::fragments($url), + 'params' => static::params($url), + 'query' => static::query($url), + 'hash' => static::hash($url), + ); + + $parts = array_merge($defaults, $parts); + $result = array(r(!empty($parts['scheme']), $parts['scheme'] . '://') . $parts['host'] . r(!empty($parts['port']), ':' . $parts['port'])); + + if(!empty($parts['fragments'])) $result[] = implode('/', $parts['fragments']); + if(!empty($parts['params'])) $result[] = static::paramsToString($parts['params']); + if(!empty($parts['query'])) $result[] = '?' . static::queryToString($parts['query']); + + return implode('/', $result) . (!empty($parts['hash']) ? '#' . $parts['hash'] : ''); + + } + + public static function queryToString($query = null) { + if(is_null($query)) $query = url::query(); + return http_build_query($query); + } + + public static function paramsToString($params = null) { + if(is_null($params)) $params = url::params(); + $result = array(); + foreach($params as $key => $val) $result[] = $key . static::paramSeparator() . $val; + return implode('/', $result); + } + + public static function stripPath($url = null) { + if(is_null($url)) $url = static::current(); + return static::build(array('fragments' => array(), 'params' => array()), $url); + } + + public static function stripFragments($url = null) { + if(is_null($url)) $url = static::current(); + return static::build(array('fragments' => array()), $url); + } + + public static function stripParams($url = null) { + if(is_null($url)) $url = static::current(); + return static::build(array('params' => array()), $url); + } + + /** + * Strips the query from the URL + * + * + * + * echo url::stripQuery('http://www.youtube.com/watch?v=9q_aXttJduk'); + * // output: http://www.youtube.com/watch + * + * + * + * @param string $url + * @return string + */ + public static function stripQuery($url = null) { + if(is_null($url)) $url = static::current(); + return static::build(array('query' => array()), $url); + } + + /** + * Strips a hash value from the URL + * + * + * + * echo url::stripHash('http://testurl.com/#somehash'); + * // output: http://testurl.com/ + * + * + * + * @param string $url + * @return string + */ + public static function stripHash($url) { + if(is_null($url)) $url = static::current(); + return static::build(array('hash' => ''), $url); + } + + /** + * Checks if an URL is absolute + * + * @return boolean + */ + public static function isAbsolute($url) { + // don't convert absolute urls + return (str::startsWith($url, 'http://') || str::startsWith($url, 'https://') || str::startsWith($url, '//')); + } + + /** + * Convert a relative path into an absolute URL + * + * @param string $path + * @param string $home + * @return string + */ + public static function makeAbsolute($path, $home = null) { + + if(static::isAbsolute($path)) return $path; + + // build the full url + $path = ltrim($path, '/'); + $home = is_null($home) ? static::$home : $home; + + if(empty($path)) return $home; + + return $home == '/' ? '/' . $path : $home . '/' . $path; + + } + + /** + * Tries to fix a broken url without protocol + * + * @param string $url + * @return string + */ + public static function fix($url) { + // 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() { + return static::$home; + } + + /** + * The url smart handler. Must be defined before + * + * @return string + */ + public static function to() { + return call_user_func_array(static::$to, func_get_args()); + } + + /** + * Return the last url the user has been on if detectable + * + * @return string + */ + public static function last() { + return r::referer(); + } + + /** + * Returns the base url + * + * @param string $url + * @return string + */ + public static function base($url = null) { + if(is_null($url)) { + $port = server::get('SERVER_PORT'); + $port = in_array($port, array(80, 443)) ? null : $port; + return static::scheme() . '://' . server::get('SERVER_NAME', server::get('SERVER_ADDR')) . r($port, ':' . $port); + } else { + $port = static::port($url); + $scheme = static::scheme($url); + $host = static::host($url) . r(is_int($port), ':' . $port); + return r($scheme, $scheme . '://') . $host; + } + } + + /** + * Shortens a URL + * It removes http:// or https:// and uses str::short afterwards + * + * + * + * echo url::short('http://veryveryverylongurl.com', 30); + * // output: veryveryverylongurl.com + * + * + * + * @param string $url The URL to be shortened + * @param int $length The final number of characters the URL should have + * @param boolean $base True: only take the base of the URL. + * @param string $rep The element, which should be added if the string is too long. Ellipsis is the default. + * @return string The shortened URL + */ + public static function short($url, $length = false, $base = false, $rep = '…') { + + if($base) $url = static::base($url); + + // replace all the nasty stuff from the url + $url = str_replace(array('http://', 'https://', 'ftp://', 'www.'), '', $url); + + // try to remove the last / after the url + $url = rtrim($url, '/'); + + return ($length) ? str::short($url, $length, $rep) : $url; + + } + + /** + * Returns the URL for document root no + * matter what the path is. + * + * @return string + */ + public static function index() { + if(r::cli()) { + return '/'; + } else { + return static::base() . preg_replace('!\/index\.php$!i', '', server::get('SCRIPT_NAME')); + } + } + +} + +// basic home url setup +url::$home = url::base(); + +// basic url generator setup +url::$to = function($path = '/') { + + if(url::isAbsolute($path)) return $path; + + $path = ltrim($path, '/'); + + if(empty($path)) return url::home(); + + return url::home() . '/' . $path; + +}; diff --git a/kirby/toolkit/lib/v.php b/kirby/toolkit/lib/v.php new file mode 100644 index 0000000..62a408d --- /dev/null +++ b/kirby/toolkit/lib/v.php @@ -0,0 +1,131 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class V { + + // an array with all installed validators + public static $validators = array(); + + /** + * Return the list of all validators + * + * @return array + */ + public static function validators() { + return static::$validators; + } + + /** + * Calls an installed validator and passes all arguments + * + * @param string $method + * @param array $arguments + * @return boolean + */ + public static function __callStatic($method, $arguments) { + + // check for missing validators + if(!isset(static::$validators[$method])) throw new Exception('The validator does not exist: ' . $method); + + return call_user_func_array(static::$validators[$method], $arguments); + + } + +} + + +/** + * Default set of validators + */ +v::$validators = array( + 'accepted' => function($value) { + return v::in($value, array(1, true, 'yes', 'true', '1', 'on')); + }, + 'denied' => function($value) { + return v::in($value, array(0, false, 'no', 'false', '0', 'off')); + }, + 'alpha' => function($value) { + return v::match($value, '/^([a-z])+$/i'); + }, + 'alphanum' => function($value) { + return v::match($value, '/^[a-z0-9]+$/i'); + }, + 'between' => function($value, $min, $max) { + return v::min($value, $min) && v::max($value, $max); + }, + 'date' => function($value) { + $time = strtotime($value); + if(!is_int($time)) return false; + + $year = date('Y', $time); + $month = date('m', $time); + $day = date('d', $time); + + return checkdate($month, $day, $year); + + }, + 'different' => function($value, $other) { + return $value !== $other; + }, + 'email' => function($value) { + return filter_var($value, FILTER_VALIDATE_EMAIL) !== false; + }, + 'filename' => function($value) { + return v::match($value, '/^[a-z0-9@._-]+$/i') && v::min($value, 2); + }, + 'in' => function($value, $in) { + return in_array($value, $in, true); + }, + 'integer' => function($value) { + return filter_var($value, FILTER_VALIDATE_INT) !== false; + }, + 'ip' => function($value) { + return filter_var($value, FILTER_VALIDATE_IP) !== false; + }, + 'match' => function($value, $preg) { + return preg_match($preg, $value) > 0; + }, + 'max' => function($value, $max) { + return size($value) <= $max; + }, + 'min' => function($value, $min) { + return size($value) >= $min; + }, + 'maxWords' => function($value, $max) { + return v::max(explode(' ', $value), $max); + }, + 'minWords' => function($value, $min) { + return v::min(explode(' ', $value), $min); + }, + 'notIn' => function($value, $notIn) { + return !v::in($value, $notIn); + }, + 'num' => function($value) { + return is_numeric($value); + }, + 'required' => function($key, $array) { + return !empty($array[$key]); + }, + 'same' => function($value, $other) { + return $value === $other; + }, + 'size' => function($value, $size) { + return size($value) == $size; + }, + 'url' => function($value) { + // 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/toolkit/lib/visitor.php b/kirby/toolkit/lib/visitor.php new file mode 100644 index 0000000..b42ff31 --- /dev/null +++ b/kirby/toolkit/lib/visitor.php @@ -0,0 +1,97 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Visitor { + + // banned ips + public static $banned = array(); + + // cache for the detected language code + protected static $acceptedLanguageCode = null; + + /** + * Returns the ip address of the current visitor + * + * @return string + */ + public static function ip() { + return getenv('REMOTE_ADDR'); + } + + /** + * Returns the user agent string of the current visitor + * + * @return string + */ + public static function ua() { + return isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : null; + } + + /** + * A more readable but longer alternative for ua() + * + * @return string + */ + public static function userAgent() { + return static::ua(); + } + + /** + * Returns the user's accepted language + * + * @return string + */ + public static function acceptedLanguage() { + return isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : null; + } + + /** + * Returns the user's accepted language code + * + * @return string + */ + public static function acceptedLanguageCode() { + if(!is_null(static::$acceptedLanguageCode)) return static::$acceptedLanguageCode; + $detected = explode(',', static::acceptedLanguage()); + $detected = explode('-', $detected[0]); + return static::$acceptedLanguageCode = strtolower($detected[0]); + } + + /** + * Returns the referrer if available + * + * @return string + */ + public static function referrer() { + return r::referer(); + } + + /** + * Nobody can remember if it is written with on or two r + * + * @return string + */ + public static function referer() { + return r::referer(); + } + + /** + * Checks if the ip of the current visitor is banned + * + * @return boolean + */ + public static function banned() { + return in_array(static::ip(), static::$banned); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/xml.php b/kirby/toolkit/lib/xml.php new file mode 100644 index 0000000..d88dc87 --- /dev/null +++ b/kirby/toolkit/lib/xml.php @@ -0,0 +1,144 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Xml { + + /** + * 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, $html = true) { + + // convert raw text to html safe text + if($html) { + $string = html::encode($string, false); + } + + // convert html entities to xml entities + return strtr($string, html::entities()); + + } + + /** + * 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) { + // convert xml entities to html entities + $string = strtr($string, static::entities()); + return html::decode($string); + } + + /** + * Parses a XML string and returns an array + * + * @param string $xml + * @return mixed + */ + public static function parse($xml) { + + $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)) ? $xml : false; + + } + + /** + * Returns a translation table of xml entities to html entities + * + * @return array + */ + public static function entities() { + return array_flip(html::entities()); + } + + /** + * Creates an XML string from an array + * + * @param array $array The source array + * @param string $tag The name of the root element + * @param boolean $head Include the xml declaration head or not + * @param string $charset The charset, which should be used for the header + * @param int $level The indendation level + * @return string The XML string + */ + public static function create($array, $tag = 'root', $head = true, $charset = 'utf-8', $tab = ' ', $level = 0) { + $result = ($level == 0 && $head) ? '' . PHP_EOL : ''; + $nlevel = ($level + 1); + $attr = '@attributes'; + $attributes = html::attr(a::get($array, $attr)); + if(count($array) == 1 and $attributes) { + // return the self closed node + return str_repeat($tab, $level) . '<' . $tag . ($attributes ? ' ' . $attributes : '') . ' />' . PHP_EOL; + } else { + $result .= str_repeat($tab, $level) . '<' . $tag . ($attributes ? ' ' . $attributes . ' ' : '') . '>' . PHP_EOL; + } + foreach($array as $key => $value) { + $key = str::lower($key); + if($key == $attr) { + continue; + } + if(is_array($value)) { + $mtags = false; + foreach($value as $key2 => $value2) { + if($key2 == $attr) { + continue; + } + if(is_array($value2)) { + $result .= static::create($value2, $key2, $head, $charset, $tab, $nlevel); + } elseif(!is_numeric($key)) { + $result .= static::create($value, $key, $head, $charset, $tab, $nlevel); + } elseif(trim($value2) != '') { + $value2 = (!strstr($value2, '' : $value2; + $result .= str_repeat($tab, $nlevel) . '<' . $key2 . '>' . $value2 . '' . PHP_EOL; + } + $mtags = true; + } + if(!$mtags && count($value) > 0) { + $result .= static::create($value, $key, $head, $charset, $tab, $nlevel); + } + } elseif(trim($value) != '') { + $value = (!strstr($value, '' : $value; + $result .= str_repeat($tab, $nlevel) . (is_numeric($key) ? '' : '<' . $key . '>') . $value . (is_numeric($key) ? '' : '') . PHP_EOL; + } + } + return $result . str_repeat($tab, $level) . '' . PHP_EOL; + } + +} diff --git a/kirby/toolkit/lib/yaml.php b/kirby/toolkit/lib/yaml.php new file mode 100644 index 0000000..b1fc8d5 --- /dev/null +++ b/kirby/toolkit/lib/yaml.php @@ -0,0 +1,57 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Yaml { + + /** + * Creates a new yaml string from an array + * + * @param array $array + * @return string + */ + public static function encode($array) { + return preg_replace('!^---\n!', '', spyc::yamldump($array)); + } + + /** + * Creates a new yaml file from an array + * + * @param string $file + * @param array $array + * @return boolean + */ + public static function write($file, $array) { + return f::write($file, static::encode($array)); + } + + /** + * Parses a yaml string and returns the array + * + * @param string $yaml + * @return array + */ + public static function decode($yaml) { + return spyc::yamlload($yaml); + } + + /** + * Reads and parses a yaml file and returns the array + * + * @param string $file + * @return array + */ + public static function read($file) { + return spyc::yamlload($file); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/readme.md b/kirby/toolkit/readme.md new file mode 100644 index 0000000..7a094e7 --- /dev/null +++ b/kirby/toolkit/readme.md @@ -0,0 +1,32 @@ +# Kirby Toolkit + +The Kirby 2 toolkit is a set of handy classes and helpers which make your life with PHP easier. + +[![Build Status](https://travis-ci.org/getkirby/toolkit.svg?branch=master)](https://travis-ci.org/getkirby/toolkit) + +## Installation + +```` +git pull https://github.com/getkirby/toolkit.git +```` + +Adding the toolkit to your app… + +```php + +``` + +## Requirements + +PHP 5.4+ + +## License + + + +## Author + +Bastian Allgeier + + + diff --git a/kirby/toolkit/vendors/abeautifulsite/SimpleImage.php b/kirby/toolkit/vendors/abeautifulsite/SimpleImage.php new file mode 100755 index 0000000..4af7f08 --- /dev/null +++ b/kirby/toolkit/vendors/abeautifulsite/SimpleImage.php @@ -0,0 +1,1285 @@ + - merging of forks, namespace support, PhpDoc editing, adaptive_resize() method, other fixes + * @license This software is licensed under the MIT license: http://opensource.org/licenses/MIT + * @copyright A Beautiful Site, LLC + * + */ + +namespace abeautifulsite; +use Exception; + +/** + * Class SimpleImage + * This class makes image manipulation in PHP as simple as possible. + * @package SimpleImage + * + */ +class SimpleImage { + + /** + * @var int Default output image quality + * + */ + public $quality = 80; + + protected $image, $filename, $original_info, $width, $height, $imagestring, $exif; + + /** + * Create instance and load an image, or create an image from scratch + * + * @param null|string $filename Path to image file (may be omitted to create image from scratch) + * @param int $width Image width (is used for creating image from scratch) + * @param int|null $height If omitted - assumed equal to $width (is used for creating image from scratch) + * @param null|string $color Hex color string, array(red, green, blue) or array(red, green, blue, alpha). + * Where red, green, blue - integers 0-255, alpha - integer 0-127
+ * (is used for creating image from scratch) + * + * @throws Exception + * + */ + public function __construct($filename = null, $width = null, $height = null, $color = null) { + if ($filename !== null) { + $this->load($filename); + } elseif ($width !== null) { + $this->create($width, $height, $color); + } + } + + /** + * Destroy image resource + * + */ + public function __destruct() { + if( get_resource_type($this->image) === 'gd' ) { + imagedestroy($this->image); + } + } + + /** + * Adaptive resize + * + * This function has been deprecated and will be removed in an upcoming release. Please + * update your code to use the `thumbnail()` method instead. The arguments for both + * methods are exactly the same. + * + * @param int $width + * @param int|null $height If omitted - assumed equal to $width + * + * @return SimpleImage + * + */ + public function adaptive_resize($width, $height = null) { + + return $this->thumbnail($width, $height); + + } + + /** + * Rotates and/or flips an image automatically so the orientation will be correct (based on exif 'Orientation') + * + * @return SimpleImage + * + */ + public function auto_orient() { + + // stop if there's no exif data + if(!isset($this->original_info['exif']['Orientation'])) { + return $this; + } + + switch ($this->original_info['exif']['Orientation']) { + case 1: + // Do nothing + break; + case 2: + // Flip horizontal + $this->flip('x'); + break; + case 3: + // Rotate 180 counterclockwise + $this->rotate(-180); + break; + case 4: + // vertical flip + $this->flip('y'); + break; + case 5: + // Rotate 90 clockwise and flip vertically + $this->flip('y'); + $this->rotate(90); + break; + case 6: + // Rotate 90 clockwise + $this->rotate(90); + break; + case 7: + // Rotate 90 clockwise and flip horizontally + $this->flip('x'); + $this->rotate(90); + break; + case 8: + // Rotate 90 counterclockwise + $this->rotate(-90); + break; + } + + return $this; + + } + + /** + * Best fit (proportionally resize to fit in specified width/height) + * + * Shrink the image proportionally to fit inside a $width x $height box + * + * @param int $max_width + * @param int $max_height + * + * @return SimpleImage + * + */ + public function best_fit($max_width, $max_height) { + + // If it already fits, there's nothing to do + if ($this->width <= $max_width && $this->height <= $max_height) { + return $this; + } + + // Determine aspect ratio + $aspect_ratio = $this->height / $this->width; + + // Make width fit into new dimensions + if ($this->width > $max_width) { + $width = $max_width; + $height = $width * $aspect_ratio; + } else { + $width = $this->width; + $height = $this->height; + } + + // Make height fit into new dimensions + if ($height > $max_height) { + $height = $max_height; + $width = $height / $aspect_ratio; + } + + return $this->resize($width, $height); + + } + + /** + * Blur + * + * @param string $type selective|gaussian + * @param int $passes Number of times to apply the filter + * + * @return SimpleImage + * + */ + public function blur($type = 'selective', $passes = 1) { + switch (strtolower($type)) { + case 'gaussian': + $type = IMG_FILTER_GAUSSIAN_BLUR; + break; + default: + $type = IMG_FILTER_SELECTIVE_BLUR; + break; + } + for ($i = 0; $i < $passes; $i++) { + imagefilter($this->image, $type); + } + return $this; + } + + /** + * Brightness + * + * @param int $level Darkest = -255, lightest = 255 + * + * @return SimpleImage + * + */ + public function brightness($level) { + imagefilter($this->image, IMG_FILTER_BRIGHTNESS, $this->keep_within($level, -255, 255)); + return $this; + } + + /** + * Contrast + * + * @param int $level Min = -100, max = 100 + * + * @return SimpleImage + * + * + */ + public function contrast($level) { + imagefilter($this->image, IMG_FILTER_CONTRAST, $this->keep_within($level, -100, 100)); + return $this; + } + + /** + * Colorize + * + * @param string $color Hex color string, array(red, green, blue) or array(red, green, blue, alpha). + * Where red, green, blue - integers 0-255, alpha - integer 0-127 + * @param float|int $opacity 0-1 + * + * @return SimpleImage + * + */ + public function colorize($color, $opacity) { + $rgba = $this->normalize_color($color); + $alpha = $this->keep_within(127 - (127 * $opacity), 0, 127); + imagefilter($this->image, IMG_FILTER_COLORIZE, $this->keep_within($rgba['r'], 0, 255), $this->keep_within($rgba['g'], 0, 255), $this->keep_within($rgba['b'], 0, 255), $alpha); + return $this; + } + + /** + * Create an image from scratch + * + * @param int $width Image width + * @param int|null $height If omitted - assumed equal to $width + * @param null|string $color Hex color string, array(red, green, blue) or array(red, green, blue, alpha). + * Where red, green, blue - integers 0-255, alpha - integer 0-127 + * + * @return SimpleImage + * + */ + public function create($width, $height = null, $color = null) { + + $height = $height ?: $width; + $this->width = $width; + $this->height = $height; + $this->image = imagecreatetruecolor($width, $height); + $this->original_info = array( + 'width' => $width, + 'height' => $height, + 'orientation' => $this->get_orientation(), + 'exif' => null, + 'format' => 'png', + 'mime' => 'image/png' + ); + + if ($color !== null) { + $this->fill($color); + } + + return $this; + + } + + /** + * Crop an image + * + * @param int $x1 Left + * @param int $y1 Top + * @param int $x2 Right + * @param int $y2 Bottom + * + * @return SimpleImage + * + */ + public function crop($x1, $y1, $x2, $y2) { + + // Determine crop size + if ($x2 < $x1) { + list($x1, $x2) = array($x2, $x1); + } + if ($y2 < $y1) { + list($y1, $y2) = array($y2, $y1); + } + $crop_width = $x2 - $x1; + $crop_height = $y2 - $y1; + + // Perform crop + $new = imagecreatetruecolor($crop_width, $crop_height); + imagealphablending($new, false); + imagesavealpha($new, true); + imagecopyresampled($new, $this->image, 0, 0, $x1, $y1, $crop_width, $crop_height, $crop_width, $crop_height); + + // Update meta data + $this->width = $crop_width; + $this->height = $crop_height; + $this->image = $new; + + return $this; + + } + + /** + * Desaturate + * + * @param int $percentage Level of desaturization. + * + * @return SimpleImage + * + */ + public function desaturate($percentage = 100) { + + // Determine percentage + $percentage = $this->keep_within($percentage, 0, 100); + + if( $percentage === 100 ) { + imagefilter($this->image, IMG_FILTER_GRAYSCALE); + } else { + // Make a desaturated copy of the image + $new = imagecreatetruecolor($this->width, $this->height); + imagealphablending($new, false); + imagesavealpha($new, true); + imagecopy($new, $this->image, 0, 0, 0, 0, $this->width, $this->height); + imagefilter($new, IMG_FILTER_GRAYSCALE); + + // Merge with specified percentage + $this->imagecopymerge_alpha($this->image, $new, 0, 0, 0, 0, $this->width, $this->height, $percentage); + imagedestroy($new); + + } + + return $this; + } + + /** + * Edge Detect + * + * @return SimpleImage + * + */ + public function edges() { + imagefilter($this->image, IMG_FILTER_EDGEDETECT); + return $this; + } + + /** + * Emboss + * + * @return SimpleImage + * + */ + public function emboss() { + imagefilter($this->image, IMG_FILTER_EMBOSS); + return $this; + } + + /** + * Fill image with color + * + * @param string $color Hex color string, array(red, green, blue) or array(red, green, blue, alpha). + * Where red, green, blue - integers 0-255, alpha - integer 0-127 + * + * @return SimpleImage + * + */ + public function fill($color = '#000000') { + + $rgba = $this->normalize_color($color); + $fill_color = imagecolorallocatealpha($this->image, $rgba['r'], $rgba['g'], $rgba['b'], $rgba['a']); + imagealphablending($this->image, false); + imagesavealpha($this->image, true); + imagefilledrectangle($this->image, 0, 0, $this->width, $this->height, $fill_color); + + return $this; + + } + + /** + * Fit to height (proportionally resize to specified height) + * + * @param int $height + * + * @return SimpleImage + * + */ + public function fit_to_height($height) { + + $aspect_ratio = $this->height / $this->width; + $width = $height / $aspect_ratio; + + return $this->resize($width, $height); + + } + + /** + * Fit to width (proportionally resize to specified width) + * + * @param int $width + * + * @return SimpleImage + * + */ + public function fit_to_width($width) { + + $aspect_ratio = $this->height / $this->width; + $height = $width * $aspect_ratio; + + return $this->resize($width, $height); + + } + + /** + * Flip an image horizontally or vertically + * + * @param string $direction x|y + * + * @return SimpleImage + * + */ + public function flip($direction) { + + $new = imagecreatetruecolor($this->width, $this->height); + imagealphablending($new, false); + imagesavealpha($new, true); + + switch (strtolower($direction)) { + case 'y': + for ($y = 0; $y < $this->height; $y++) { + imagecopy($new, $this->image, 0, $y, 0, $this->height - $y - 1, $this->width, 1); + } + break; + default: + for ($x = 0; $x < $this->width; $x++) { + imagecopy($new, $this->image, $x, 0, $this->width - $x - 1, 0, 1, $this->height); + } + break; + } + + $this->image = $new; + + return $this; + + } + + /** + * Get the current height + * + * @return int + * + */ + public function get_height() { + return $this->height; + } + + /** + * Get the current orientation + * + * @return string portrait|landscape|square + * + */ + public function get_orientation() { + + if (imagesx($this->image) > imagesy($this->image)) { + return 'landscape'; + } + + if (imagesx($this->image) < imagesy($this->image)) { + return 'portrait'; + } + + return 'square'; + + } + + /** + * Get info about the original image + * + * @return array
 array(
+     *  width        => 320,
+     *  height       => 200,
+     *  orientation  => ['portrait', 'landscape', 'square'],
+     *  exif         => array(...),
+     *  mime         => ['image/jpeg', 'image/gif', 'image/png'],
+     *  format       => ['jpeg', 'gif', 'png']
+     * )
+ * + */ + public function get_original_info() { + return $this->original_info; + } + + /** + * Get the current width + * + * @return int + * + */ + public function get_width() { + return $this->width; + } + + /** + * Invert + * + * @return SimpleImage + * + */ + public function invert() { + imagefilter($this->image, IMG_FILTER_NEGATE); + return $this; + } + + /** + * Load an image + * + * @param string $filename Path to image file + * + * @return SimpleImage + * @throws Exception + * + */ + public function load($filename) { + + // Require GD library + if (!extension_loaded('gd')) { + throw new Exception('Required extension GD is not loaded.'); + } + $this->filename = $filename; + return $this->get_meta_data(); + } + + /** + * Load a base64 string as image + * + * @param string $base64string base64 string + * + * @return SimpleImage + * + */ + public function load_base64($base64string) { + if (!extension_loaded('gd')) { + throw new Exception('Required extension GD is not loaded.'); + } + //remove data URI scheme and spaces from base64 string then decode it + $this->imagestring = base64_decode(str_replace(' ', '+',preg_replace('#^data:image/[^;]+;base64,#', '', $base64string))); + $this->image = imagecreatefromstring($this->imagestring); + return $this->get_meta_data(); + } + + /** + * Mean Remove + * + * @return SimpleImage + * + */ + public function mean_remove() { + imagefilter($this->image, IMG_FILTER_MEAN_REMOVAL); + return $this; + } + + /** + * Changes the opacity level of the image + * + * @param float|int $opacity 0-1 + * + * @throws Exception + * + */ + public function opacity($opacity) { + + // Determine opacity + $opacity = $this->keep_within($opacity, 0, 1) * 100; + + // Make a copy of the image + $copy = imagecreatetruecolor($this->width, $this->height); + imagealphablending($copy, false); + imagesavealpha($copy, true); + imagecopy($copy, $this->image, 0, 0, 0, 0, $this->width, $this->height); + + // Create transparent layer + $this->create($this->width, $this->height, array(0, 0, 0, 127)); + + // Merge with specified opacity + $this->imagecopymerge_alpha($this->image, $copy, 0, 0, 0, 0, $this->width, $this->height, $opacity); + imagedestroy($copy); + + return $this; + + } + + /** + * Outputs image without saving + * + * @param null|string $format If omitted or null - format of original file will be used, may be gif|jpg|png + * @param int|null $quality Output image quality in percents 0-100 + * + * @throws Exception + * + */ + public function output($format = null, $quality = null) { + + // Determine quality + $quality = $quality ?: $this->quality; + + // Determine mimetype + switch (strtolower($format)) { + case 'gif': + $mimetype = 'image/gif'; + break; + case 'jpeg': + case 'jpg': + imageinterlace($this->image, true); + $mimetype = 'image/jpeg'; + break; + case 'png': + $mimetype = 'image/png'; + break; + default: + $info = (empty($this->imagestring)) ? getimagesize($this->filename) : getimagesizefromstring($this->imagestring); + $mimetype = $info['mime']; + unset($info); + break; + } + + // Output the image + header('Content-Type: '.$mimetype); + switch ($mimetype) { + case 'image/gif': + imagegif($this->image); + break; + case 'image/jpeg': + imagejpeg($this->image, null, round($quality)); + break; + case 'image/png': + imagepng($this->image, null, round(9 * $quality / 100)); + break; + default: + throw new Exception('Unsupported image format: '.$this->filename); + } + } + + /** + * Outputs image as data base64 to use as img src + * + * @param null|string $format If omitted or null - format of original file will be used, may be gif|jpg|png + * @param int|null $quality Output image quality in percents 0-100 + * + * @return string + * @throws Exception + * + */ + public function output_base64($format = null, $quality = null) { + + // Determine quality + $quality = $quality ?: $this->quality; + + // Determine mimetype + switch (strtolower($format)) { + case 'gif': + $mimetype = 'image/gif'; + break; + case 'jpeg': + case 'jpg': + imageinterlace($this->image, true); + $mimetype = 'image/jpeg'; + break; + case 'png': + $mimetype = 'image/png'; + break; + default: + $info = getimagesize($this->filename); + $mimetype = $info['mime']; + unset($info); + break; + } + + // Output the image + ob_start(); + switch ($mimetype) { + case 'image/gif': + imagegif($this->image); + break; + case 'image/jpeg': + imagejpeg($this->image, null, round($quality)); + break; + case 'image/png': + imagepng($this->image, null, round(9 * $quality / 100)); + break; + default: + throw new Exception('Unsupported image format: '.$this->filename); + } + $image_data = ob_get_contents(); + ob_end_clean(); + + // Returns formatted string for img src + return 'data:'.$mimetype.';base64,'.base64_encode($image_data); + + } + + /** + * Overlay + * + * Overlay an image on top of another, works with 24-bit PNG alpha-transparency + * + * @param string $overlay An image filename or a SimpleImage object + * @param string $position center|top|left|bottom|right|top left|top right|bottom left|bottom right + * @param float|int $opacity Overlay opacity 0-1 + * @param int $x_offset Horizontal offset in pixels + * @param int $y_offset Vertical offset in pixels + * + * @return SimpleImage + * + */ + public function overlay($overlay, $position = 'center', $opacity = 1, $x_offset = 0, $y_offset = 0) { + + // Load overlay image + if( !($overlay instanceof SimpleImage) ) { + $overlay = new SimpleImage($overlay); + } + + // Convert opacity + $opacity = $opacity * 100; + + // Determine position + switch (strtolower($position)) { + case 'top left': + $x = 0 + $x_offset; + $y = 0 + $y_offset; + break; + case 'top right': + $x = $this->width - $overlay->width + $x_offset; + $y = 0 + $y_offset; + break; + case 'top': + $x = ($this->width / 2) - ($overlay->width / 2) + $x_offset; + $y = 0 + $y_offset; + break; + case 'bottom left': + $x = 0 + $x_offset; + $y = $this->height - $overlay->height + $y_offset; + break; + case 'bottom right': + $x = $this->width - $overlay->width + $x_offset; + $y = $this->height - $overlay->height + $y_offset; + break; + case 'bottom': + $x = ($this->width / 2) - ($overlay->width / 2) + $x_offset; + $y = $this->height - $overlay->height + $y_offset; + break; + case 'left': + $x = 0 + $x_offset; + $y = ($this->height / 2) - ($overlay->height / 2) + $y_offset; + break; + case 'right': + $x = $this->width - $overlay->width + $x_offset; + $y = ($this->height / 2) - ($overlay->height / 2) + $y_offset; + break; + case 'center': + default: + $x = ($this->width / 2) - ($overlay->width / 2) + $x_offset; + $y = ($this->height / 2) - ($overlay->height / 2) + $y_offset; + break; + } + + // Perform the overlay + $this->imagecopymerge_alpha($this->image, $overlay->image, $x, $y, 0, 0, $overlay->width, $overlay->height, $opacity); + + return $this; + + } + + /** + * Pixelate + * + * @param int $block_size Size in pixels of each resulting block + * + * @return SimpleImage + * + */ + public function pixelate($block_size = 10) { + imagefilter($this->image, IMG_FILTER_PIXELATE, $block_size, true); + return $this; + } + + /** + * Resize an image to the specified dimensions + * + * @param int $width + * @param int $height + * + * @return SimpleImage + * + */ + public function resize($width, $height) { + + // Generate new GD image + $new = imagecreatetruecolor($width, $height); + + if( $this->original_info['format'] === 'gif' ) { + // Preserve transparency in GIFs + $transparent_index = imagecolortransparent($this->image); + $palletsize = imagecolorstotal($this->image); + if ($transparent_index >= 0 && $transparent_index < $palletsize) { + $transparent_color = imagecolorsforindex($this->image, $transparent_index); + $transparent_index = imagecolorallocate($new, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']); + imagefill($new, 0, 0, $transparent_index); + imagecolortransparent($new, $transparent_index); + } + } else { + // Preserve transparency in PNGs (benign for JPEGs) + imagealphablending($new, false); + imagesavealpha($new, true); + } + + // Resize + imagecopyresampled($new, $this->image, 0, 0, 0, 0, $width, $height, $this->width, $this->height); + + // Update meta data + $this->width = $width; + $this->height = $height; + $this->image = $new; + + return $this; + + } + + /** + * Rotate an image + * + * @param int $angle 0-360 + * @param string $bg_color Hex color string, array(red, green, blue) or array(red, green, blue, alpha). + * Where red, green, blue - integers 0-255, alpha - integer 0-127 + * + * @return SimpleImage + * + */ + public function rotate($angle, $bg_color = '#000000') { + + // Perform the rotation + $rgba = $this->normalize_color($bg_color); + $bg_color = imagecolorallocatealpha($this->image, $rgba['r'], $rgba['g'], $rgba['b'], $rgba['a']); + $new = imagerotate($this->image, -($this->keep_within($angle, -360, 360)), $bg_color); + imagesavealpha($new, true); + imagealphablending($new, true); + + // Update meta data + $this->width = imagesx($new); + $this->height = imagesy($new); + $this->image = $new; + + return $this; + + } + + /** + * Save an image + * + * The resulting format will be determined by the file extension. + * + * @param null|string $filename If omitted - original file will be overwritten + * @param null|int $quality Output image quality in percents 0-100 + * @param null|string $format The format to use; determined by file extension if null + * + * @return SimpleImage + * @throws Exception + * + */ + public function save($filename = null, $quality = null, $format = null) { + + // Determine quality, filename, and format + $quality = $quality ?: $this->quality; + $filename = $filename ?: $this->filename; + if( $format === null ) { + $format = $this->file_ext($filename) ?: $this->original_info['format']; + } + + // Create the image + switch (strtolower($format)) { + case 'gif': + $result = imagegif($this->image, $filename); + break; + case 'jpg': + case 'jpeg': + imageinterlace($this->image, true); + $result = imagejpeg($this->image, $filename, round($quality)); + break; + case 'png': + $result = imagepng($this->image, $filename, round(9 * $quality / 100)); + break; + default: + throw new Exception('Unsupported format'); + } + + if (!$result) { + throw new Exception('Unable to save image: ' . $filename); + } + + return $this; + + } + + /** + * Sepia + * + * @return SimpleImage + * + */ + public function sepia() { + imagefilter($this->image, IMG_FILTER_GRAYSCALE); + imagefilter($this->image, IMG_FILTER_COLORIZE, 100, 50, 0); + return $this; + } + + /** + * Sketch + * + * @return SimpleImage + * + */ + public function sketch() { + imagefilter($this->image, IMG_FILTER_MEAN_REMOVAL); + return $this; + } + + /** + * Smooth + * + * @param int $level Min = -10, max = 10 + * + * @return SimpleImage + * + */ + public function smooth($level) { + imagefilter($this->image, IMG_FILTER_SMOOTH, $this->keep_within($level, -10, 10)); + return $this; + } + + /** + * Add text to an image + * + * @param string $text + * @param string $font_file + * @param float|int $font_size + * @param string $color + * @param string $position + * @param int $x_offset + * @param int $y_offset + * + * @return SimpleImage + * @throws Exception + * + */ + public function text($text, $font_file, $font_size = 12, $color = '#000000', $position = 'center', $x_offset = 0, $y_offset = 0) { + + // todo - this method could be improved to support the text angle + $angle = 0; + + // Determine text color + $rgba = $this->normalize_color($color); + $color = imagecolorallocatealpha($this->image, $rgba['r'], $rgba['g'], $rgba['b'], $rgba['a']); + + // Determine textbox size + $box = imagettfbbox($font_size, $angle, $font_file, $text); + if (empty($box)) { + throw new Exception('Unable to load font: '.$font_file); + } + $box_width = abs($box[6] - $box[2]); + $box_height = abs($box[7] - $box[1]); + + // Determine position + switch (strtolower($position)) { + case 'top left': + $x = 0 + $x_offset; + $y = 0 + $y_offset + $box_height; + break; + case 'top right': + $x = $this->width - $box_width + $x_offset; + $y = 0 + $y_offset + $box_height; + break; + case 'top': + $x = ($this->width / 2) - ($box_width / 2) + $x_offset; + $y = 0 + $y_offset + $box_height; + break; + case 'bottom left': + $x = 0 + $x_offset; + $y = $this->height - $box_height + $y_offset + $box_height; + break; + case 'bottom right': + $x = $this->width - $box_width + $x_offset; + $y = $this->height - $box_height + $y_offset + $box_height; + break; + case 'bottom': + $x = ($this->width / 2) - ($box_width / 2) + $x_offset; + $y = $this->height - $box_height + $y_offset + $box_height; + break; + case 'left': + $x = 0 + $x_offset; + $y = ($this->height / 2) - (($box_height / 2) - $box_height) + $y_offset; + break; + case 'right'; + $x = $this->width - $box_width + $x_offset; + $y = ($this->height / 2) - (($box_height / 2) - $box_height) + $y_offset; + break; + case 'center': + default: + $x = ($this->width / 2) - ($box_width / 2) + $x_offset; + $y = ($this->height / 2) - (($box_height / 2) - $box_height) + $y_offset; + break; + } + + // Add the text + imagesavealpha($this->image, true); + imagealphablending($this->image, true); + imagettftext($this->image, $font_size, $angle, $x, $y, $color, $font_file, $text); + + return $this; + + } + + /** + * Thumbnail + * + * This function attempts to get the image to as close to the provided dimensions as possible, and then crops the + * remaining overflow (from the center) to get the image to be the size specified. Useful for generating thumbnails. + * + * @param int $width + * @param int|null $height If omitted - assumed equal to $width + * + * @return SimpleImage + * + */ + public function thumbnail($width, $height = null) { + + // Determine height + $height = $height ?: $width; + + // Determine aspect ratios + $current_aspect_ratio = $this->height / $this->width; + $new_aspect_ratio = $height / $width; + + // Fit to height/width + if ($new_aspect_ratio > $current_aspect_ratio) { + $this->fit_to_height($height); + } else { + $this->fit_to_width($width); + } + $left = floor(($this->width / 2) - ($width / 2)); + $top = floor(($this->height / 2) - ($height / 2)); + + // Return trimmed image + return $this->crop($left, $top, $width + $left, $height + $top); + + } + + /** + * Returns the file extension of the specified file + * + * @param string $filename + * + * @return string + * + */ + protected function file_ext($filename) { + + if (!preg_match('/\./', $filename)) { + return ''; + } + + return preg_replace('/^.*\./', '', $filename); + + } + + /** + * Get meta data of image or base64 string + * + * @return SimpleImage + * @throws Exception + * + */ + protected function get_meta_data() { + //gather meta data + if(empty($this->imagestring)) { + $info = getimagesize($this->filename); + + switch ($info['mime']) { + case 'image/gif': + $this->image = imagecreatefromgif($this->filename); + break; + case 'image/jpeg': + $this->image = imagecreatefromjpeg($this->filename); + break; + case 'image/png': + $this->image = imagecreatefrompng($this->filename); + break; + default: + throw new Exception('Invalid image: '.$this->filename); + } + } elseif (function_exists('getimagesizefromstring')) { + $info = getimagesizefromstring($this->imagestring); + } else { + throw new Exception('PHP 5.4 is required to use method getimagesizefromstring'); + } + + $this->original_info = array( + 'width' => $info[0], + 'height' => $info[1], + 'orientation' => $this->get_orientation(), + 'exif' => function_exists('exif_read_data') && $info['mime'] === 'image/jpeg' && $this->imagestring === null ? $this->exif = @exif_read_data($this->filename) : null, + 'format' => preg_replace('/^image\//', '', $info['mime']), + 'mime' => $info['mime'] + ); + $this->width = $info[0]; + $this->height = $info[1]; + + imagesavealpha($this->image, true); + imagealphablending($this->image, true); + + return $this; + + } + + /** + * Same as PHP's imagecopymerge() function, except preserves alpha-transparency in 24-bit PNGs + * + * @param $dst_im + * @param $src_im + * @param $dst_x + * @param $dst_y + * @param $src_x + * @param $src_y + * @param $src_w + * @param $src_h + * @param $pct + * + * @link http://www.php.net/manual/en/function.imagecopymerge.php#88456 + * + */ + protected function imagecopymerge_alpha($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct) { + + // Get image width and height and percentage + $pct /= 100; + $w = imagesx($src_im); + $h = imagesy($src_im); + + // Turn alpha blending off + imagealphablending($src_im, false); + + // Find the most opaque pixel in the image (the one with the smallest alpha value) + $minalpha = 127; + for ($x = 0; $x < $w; $x++) { + for ($y = 0; $y < $h; $y++) { + $alpha = (imagecolorat($src_im, $x, $y) >> 24) & 0xFF; + if ($alpha < $minalpha) { + $minalpha = $alpha; + } + } + } + + // Loop through image pixels and modify alpha for each + for ($x = 0; $x < $w; $x++) { + for ($y = 0; $y < $h; $y++) { + // Get current alpha value (represents the TANSPARENCY!) + $colorxy = imagecolorat($src_im, $x, $y); + $alpha = ($colorxy >> 24) & 0xFF; + // Calculate new alpha + if ($minalpha !== 127) { + $alpha = 127 + 127 * $pct * ($alpha - 127) / (127 - $minalpha); + } else { + $alpha += 127 * $pct; + } + // Get the color index with new alpha + $alphacolorxy = imagecolorallocatealpha($src_im, ($colorxy >> 16) & 0xFF, ($colorxy >> 8) & 0xFF, $colorxy & 0xFF, $alpha); + // Set pixel with the new color + opacity + if (!imagesetpixel($src_im, $x, $y, $alphacolorxy)) { + return; + } + } + } + + // Copy it + imagesavealpha($dst_im, true); + imagealphablending($dst_im, true); + imagesavealpha($src_im, true); + imagealphablending($src_im, true); + imagecopy($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h); + + } + + /** + * Ensures $value is always within $min and $max range. + * + * If lower, $min is returned. If higher, $max is returned. + * + * @param int|float $value + * @param int|float $min + * @param int|float $max + * + * @return int|float + * + */ + protected function keep_within($value, $min, $max) { + + if ($value < $min) { + return $min; + } + + if ($value > $max) { + return $max; + } + + return $value; + + } + + /** + * Converts a hex color value to its RGB equivalent + * + * @param string $color Hex color string, array(red, green, blue) or array(red, green, blue, alpha). + * Where red, green, blue - integers 0-255, alpha - integer 0-127 + * + * @return array|bool + * + */ + protected function normalize_color($color) { + + if (is_string($color)) { + + $color = trim($color, '#'); + + if (strlen($color) == 6) { + list($r, $g, $b) = array( + $color[0].$color[1], + $color[2].$color[3], + $color[4].$color[5] + ); + } elseif (strlen($color) == 3) { + list($r, $g, $b) = array( + $color[0].$color[0], + $color[1].$color[1], + $color[2].$color[2] + ); + } else { + return false; + } + return array( + 'r' => hexdec($r), + 'g' => hexdec($g), + 'b' => hexdec($b), + 'a' => 0 + ); + + } elseif (is_array($color) && (count($color) == 3 || count($color) == 4)) { + + if (isset($color['r'], $color['g'], $color['b'])) { + return array( + 'r' => $this->keep_within($color['r'], 0, 255), + 'g' => $this->keep_within($color['g'], 0, 255), + 'b' => $this->keep_within($color['b'], 0, 255), + 'a' => $this->keep_within(isset($color['a']) ? $color['a'] : 0, 0, 127) + ); + } elseif (isset($color[0], $color[1], $color[2])) { + return array( + 'r' => $this->keep_within($color[0], 0, 255), + 'g' => $this->keep_within($color[1], 0, 255), + 'b' => $this->keep_within($color[2], 0, 255), + 'a' => $this->keep_within(isset($color[3]) ? $color[3] : 0, 0, 127) + ); + } + + } + return false; + } + +} diff --git a/kirby/toolkit/vendors/mimereader/mimereader.php b/kirby/toolkit/vendors/mimereader/mimereader.php new file mode 100644 index 0000000..6752376 --- /dev/null +++ b/kirby/toolkit/vendors/mimereader/mimereader.php @@ -0,0 +1,776 @@ +file = $file; + + if ( empty( self::$binary_characters ) ) { + self::$binary_characters .= "\x00\x01\x02\x03\x04\x05\x06\x07\0x08\x0B\x0E\x0F\x10\x11"; + self::$binary_characters .= "\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1C\x1D\x1E\x1F"; + } + if ( empty( self::$whitespace_characters ) ) { + self::$whitespace_characters .= "\x09\x0A\x0C\x0D\x20"; + } + if ( empty( self::$tag_terminating_characters ) ) { + self::$tag_terminating_characters .= "\x20\x3E"; + } + + if ( is_null( self::$image ) ) { + $image = &self::$image; + $image = array(); + + // Windows Icon + $image[] = array ( + 'mime' => 'image/vnd.microsoft.icon', + 'pattern' => "\x00\x00\x01\x00", + 'mask' => "\xFF\xFF\xFF\xFF", + 'ignore' => '', // none + ); + // "BM" - BMP signature + $image[] = array ( + 'mime' => 'image/bmp', + 'pattern' => "\x42\x4D", + 'mask' => "\xFF\xFF", + 'ignore' => '' + ); + // "GIF87a" - GIF signature + $image[] = array ( + 'mime' => 'image/gif', + 'pattern' => "\x47\x49\x46\x38\x37\x61", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // "GIF89a" - GIF signature + $image[] = array ( + 'mime' => 'image/gif', + 'pattern' => "\x47\x49\x46\x38\x39\x61", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // "RIFF" followed by 4 bytes followed by "WEBPVP" + $image[] = array ( + 'mime' => 'image/webp', + 'pattern' => "\x52\x49\x46\x46\x00\x00\x00\x00\x57\x45\x42\x50\x56\x50", + 'mask' => "\xFF\xFF\xFF\xFF\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // A byte with only the highest bit set followed by the string "PNG" followed by CR LF SUB LF - PNG signature + $image[] = array ( + 'mime' => 'image/png', + 'pattern' => "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // JPEG start of image marker followed by another marker + $image[] = array ( + 'mime' => 'image/jpeg', + 'pattern' => "\xFF\xD8\xFF", + 'mask' => "\xFF\xFF\xFF", + 'ignore' => '' + ); + // PSD signature + $image[] = array ( + 'mime' => 'application/psd', + 'pattern' => "\x38\x42\x50\x53", + 'mask' => "\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + } + if ( is_null( self::$media ) ) { + $media = &self::$media; + $media = array(); + + // The WebM signature + $media[] = array ( + 'mime' => 'video/webm', + 'pattern' => "\x1A\x45\xDF\xA3", + 'mask' => "\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // The .snd signature + $media[] = array ( + 'mime' => 'audio/basic', + 'pattern' => "\x2E\x73\x6E\x64", + 'mask' => "\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // "FORM" followed by 4 bytes followed by "AIFF" - the AIFF signature + $media[] = array ( + 'mime' => 'audio/aiff', + 'pattern' => "\x46\x4F\x52\x4D\x00\x00\x00\x00\x41\x49\x46\x46", + 'mask' => "\xFF\xFF\xFF\xFF\x00\x00\x00\x00\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // MP3 without ID3 tag /****** UNTESTED ******/ + $media[] = array ( + 'mime' => 'audio/mpeg', + 'pattern' => "\xFF\xFB", + 'mask' => "\xFF\xFF", + 'ignore' => '' + ); + // "ID3" and the ID3v2-tagged MP3 signature + $media[] = array ( + 'mime' => 'audio/mpeg', + 'pattern' => "\x49\x44\x33", + 'mask' => "\xFF\xFF\xFF", + 'ignore' => '' + ); + // "OggS" followed by NUL - The OGG signature + $media[] = array ( + 'mime' => 'application/ogg', + 'pattern' => "\x4F\x67\x67\x53\x00", + 'mask' => "\xFF\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // "MThd" followed by 4 bytes representing the number 6 in 32 bits (big endian) - MIDI signature + $media[] = array ( + 'mime' => 'audio/midi', + 'pattern' => "\x4D\x54\x68\x64\x00\x00\x00\x06", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // "RIFF" followed by 4 bytes followed by "AVI" - AVI signature + $media[] = array ( + 'mime' => 'video/avi', + 'pattern' => "\x52\x49\x46\x46\x00\x00\x00\x00\x41\x56\x49\x20", + 'mask' => "\xFF\xFF\xFF\xFF\x00\x00\x00\x00\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // "RIFF" followed by 4 bytes followed by "WAVE" - WAVE signature + $media[] = array ( + 'mime' => 'audio/wave', + 'pattern' => "\x52\x49\x46\x46\x00\x00\x00\x00\x57\x41\x56\x45", + 'mask' => "\xFF\xFF\xFF\xFF\x00\x00\x00\x00\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + } + if ( is_null( self::$fonts ) ) { + $fonts = &self::$fonts; + $fonts = array(); + + // 34 bytes followed by "LP" - Opentype signature + $fonts[] = array ( + 'mime' => 'application/vnd.ms-fontobject', + 'pattern' => "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" . + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x4C\x50", + 'mask' => "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" . + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF", + 'ignore' => '' + ); + // 4 bytes representing version type 1 of true type font + $fonts[] = array ( + 'mime' => 'application/font-ttf', + 'pattern' => "\x00\x01\x00\x00", + 'mask' => "\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // "OTTO" - Opentype signature + $fonts[] = array ( + 'mime' => 'application/font-off', // application/vnd.ms-opentype + 'pattern' => "\x4F\x54\x54\x4F", + 'mask' => "\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // "ttcf" - Truetype Collection signature + $fonts[] = array ( + 'mime' => 'application/x-font-truetype-collection', + 'pattern' => "\x74\x74\x63\x66", + 'mask' => "\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // 'wOFF' - Web Open Font Format signature + $fonts[] = array ( + 'mime' => 'application/font-woff', + 'pattern' => "\x77\x4F\x46\x46", + 'mask' => "\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + } + if ( is_null( self::$archive ) ) { + $archive = &self::$archive; + $archive = array(); + + // GZIP signature + $archive[] = array ( + 'mime' => 'application/x-gzip', + 'pattern' => "\x1F\x8B\x08", + 'mask' => "\xFF\xFF\xFF", + 'ignore' => '' + ); + // "PK" followed by ETX, EOT - ZIP signature + $archive[] = array ( + 'mime' => 'application/zip', + 'pattern' => "\x50\x4B\x03\x04", + 'mask' => "\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // "Rar " followed by SUB, BEL, NUL - RAR signature + $archive[] = array ( + 'mime' => 'application/x-rar-compressed', + 'pattern' => "\x52\x61\x72\x20\x1A\x07\x00", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + } + if ( is_null( self::$text ) ) { + $text = &self::$text; + $text = array(); + + // "%!PS-Adobe-" - Postscript signature + $text[] = array ( + 'mime' => 'application/postscript', + 'pattern' => "\x25\x50\x53\x2D\x41\x64\x6F\x62\x65", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // UTF-16 Big Endian BOM text + $text[] = array ( + 'mime' => 'text/plain', + 'pattern' => "\xFF\xFE", + 'mask' => "\xFF\xFF", + 'ignore' => '' + ); + // UTF-16 Little Endian BOM text + $text[] = array ( + 'mime' => 'text/plain', + 'pattern' => "\xFE\xFF", + 'mask' => "\xFF\xFF", + 'ignore' => '' + ); + // UTF-8 BOM text + $text[] = array ( + 'mime' => 'text/plain', + 'pattern' => "\xEF\xBB\xBF", + 'mask' => "\xFF\xFF\xFF", + 'ignore' => '' + ); + } + if ( is_null( self::$others ) ) { + $others = &self::$others; + $others = array(); + + $others[] = array ( + 'mime' => 'WINDOWS EXECUTABLE', + 'pattern' => "\x4D\x5A", + 'mask' => "\xFF\xFF", + 'ignore' => '' + ); + $others[] = array ( + 'mime' => 'EXEC_LINKABLE', + 'pattern' => "\x7F\x45\x4C\x46", + 'mask' => "\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + + } + if ( is_null( self::$unknown ) ) { + $unknown = &self::$unknown; + $unknown = array(); + + // " 'text/html', + 'pattern' => "\x3C\x21\x44\x4F\x43\x54\x59\x50\x45\x20\x48\x54\x4D\x4C", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x48\x54\x4D\x4C", + 'mask' => "\xFF\xFF\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x48\x45\x41\x44", + 'mask' => "\xFF\xFF\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x53\x43\x52\x49\x50\x54", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x49\x46\x52\x41\x4D\x45", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x48\x31", + 'mask' => "\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x44\x49\x56", + 'mask' => "\xFF\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x46\x4F\x4E\x54", + 'mask' => "\xFF\xFF\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x54\x41\x42\x4C\x45", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x41", + 'mask' => "\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x53\x54\x59\x4C\x45", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x54\x49\x54\x4C\x45", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x42", + 'mask' => "\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x42\x4F\x44\x59", + 'mask' => "\xFF\xFF\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x42\x52", + 'mask' => "\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x50", + 'mask' => "\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // "$/', $Line['text'])) + { + $Block['closed'] = true; + } + + return $Block; + } + } + + protected function blockCommentContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + $Block['markup'] .= "\n" . $Line['body']; + + if (preg_match('/-->$/', $Line['text'])) + { + $Block['closed'] = true; + } + + return $Block; + } + + # + # Fenced Code + + protected function blockFencedCode($Line) + { + if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches)) + { + $Element = array( + 'name' => 'code', + 'text' => '', + ); + + if (isset($matches[1])) + { + $class = 'language-'.$matches[1]; + + $Element['attributes'] = array( + 'class' => $class, + ); + } + + $Block = array( + 'char' => $Line['text'][0], + 'element' => array( + 'name' => 'pre', + 'handler' => 'element', + 'text' => $Element, + ), + ); + + return $Block; + } + } + + protected function blockFencedCodeContinue($Line, $Block) + { + if (isset($Block['complete'])) + { + return; + } + + if (isset($Block['interrupted'])) + { + $Block['element']['text']['text'] .= "\n"; + + unset($Block['interrupted']); + } + + if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text'])) + { + $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1); + + $Block['complete'] = true; + + return $Block; + } + + $Block['element']['text']['text'] .= "\n".$Line['body'];; + + return $Block; + } + + protected function blockFencedCodeComplete($Block) + { + $text = $Block['element']['text']['text']; + + $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8'); + + $Block['element']['text']['text'] = $text; + + return $Block; + } + + # + # Header + + protected function blockHeader($Line) + { + if (isset($Line['text'][1])) + { + $level = 1; + + while (isset($Line['text'][$level]) and $Line['text'][$level] === '#') + { + $level ++; + } + + if ($level > 6) + { + return; + } + + $text = trim($Line['text'], '# '); + + $Block = array( + 'element' => array( + 'name' => 'h' . min(6, $level), + 'text' => $text, + 'handler' => 'line', + ), + ); + + return $Block; + } + } + + # + # List + + protected function blockList($Line) + { + list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]'); + + if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches)) + { + $Block = array( + 'indent' => $Line['indent'], + 'pattern' => $pattern, + 'element' => array( + 'name' => $name, + 'handler' => 'elements', + ), + ); + + $Block['li'] = array( + 'name' => 'li', + 'handler' => 'li', + 'text' => array( + $matches[2], + ), + ); + + $Block['element']['text'] []= & $Block['li']; + + return $Block; + } + } + + protected function blockListContinue($Line, array $Block) + { + if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches)) + { + if (isset($Block['interrupted'])) + { + $Block['li']['text'] []= ''; + + unset($Block['interrupted']); + } + + unset($Block['li']); + + $text = isset($matches[1]) ? $matches[1] : ''; + + $Block['li'] = array( + 'name' => 'li', + 'handler' => 'li', + 'text' => array( + $text, + ), + ); + + $Block['element']['text'] []= & $Block['li']; + + return $Block; + } + + if ($Line['text'][0] === '[' and $this->blockReference($Line)) + { + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); + + $Block['li']['text'] []= $text; + + return $Block; + } + + if ($Line['indent'] > 0) + { + $Block['li']['text'] []= ''; + + $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); + + $Block['li']['text'] []= $text; + + unset($Block['interrupted']); + + return $Block; + } + } + + # + # Quote + + protected function blockQuote($Line) + { + if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) + { + $Block = array( + 'element' => array( + 'name' => 'blockquote', + 'handler' => 'lines', + 'text' => (array) $matches[1], + ), + ); + + return $Block; + } + } + + protected function blockQuoteContinue($Line, array $Block) + { + if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) + { + if (isset($Block['interrupted'])) + { + $Block['element']['text'] []= ''; + + unset($Block['interrupted']); + } + + $Block['element']['text'] []= $matches[1]; + + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $Block['element']['text'] []= $Line['text']; + + return $Block; + } + } + + # + # Rule + + protected function blockRule($Line) + { + if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text'])) + { + $Block = array( + 'element' => array( + 'name' => 'hr' + ), + ); + + return $Block; + } + } + + # + # Setext + + protected function blockSetextHeader($Line, array $Block = null) + { + if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) + { + return; + } + + if (chop($Line['text'], $Line['text'][0]) === '') + { + $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; + + return $Block; + } + } + + # + # Markup + + protected function blockMarkup($Line) + { + if ($this->markupEscaped) + { + 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], + 'depth' => 0, + 'markup' => $Line['text'], + ); + + $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['markup'] .= "\n"; + + unset($Block['interrupted']); + } + + $Block['markup'] .= "\n".$Line['body']; + + return $Block; + } + + # + # Reference + + protected function blockReference($Line) + { + if (preg_match('/^\[(.+?)\]:[ ]*?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches)) + { + $id = strtolower($matches[1]); + + $Data = array( + 'url' => $matches[2], + 'title' => null, + ); + + if (isset($matches[3])) + { + $Data['title'] = $matches[3]; + } + + $this->DefinitionData['Reference'][$id] = $Data; + + $Block = array( + 'hidden' => true, + ); + + return $Block; + } + } + + # + # Table + + protected function blockTable($Line, array $Block = null) + { + if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) + { + return; + } + + if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '') + { + $alignments = array(); + + $divider = $Line['text']; + + $divider = trim($divider); + $divider = trim($divider, '|'); + + $dividerCells = explode('|', $divider); + + foreach ($dividerCells as $dividerCell) + { + $dividerCell = trim($dividerCell); + + if ($dividerCell === '') + { + continue; + } + + $alignment = null; + + if ($dividerCell[0] === ':') + { + $alignment = 'left'; + } + + if (substr($dividerCell, - 1) === ':') + { + $alignment = $alignment === 'left' ? 'center' : 'right'; + } + + $alignments []= $alignment; + } + + # ~ + + $HeaderElements = array(); + + $header = $Block['element']['text']; + + $header = trim($header); + $header = trim($header, '|'); + + $headerCells = explode('|', $header); + + foreach ($headerCells as $index => $headerCell) + { + $headerCell = trim($headerCell); + + $HeaderElement = array( + 'name' => 'th', + 'text' => $headerCell, + 'handler' => 'line', + ); + + 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', + 'handler' => 'elements', + ), + ); + + $Block['element']['text'] []= array( + 'name' => 'thead', + 'handler' => 'elements', + ); + + $Block['element']['text'] []= array( + 'name' => 'tbody', + 'handler' => 'elements', + 'text' => array(), + ); + + $Block['element']['text'][0]['text'] []= array( + 'name' => 'tr', + 'handler' => 'elements', + 'text' => $HeaderElements, + ); + + return $Block; + } + } + + protected function blockTableContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + if ($Line['text'][0] === '|' or strpos($Line['text'], '|')) + { + $Elements = array(); + + $row = $Line['text']; + + $row = trim($row); + $row = trim($row, '|'); + + preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches); + + foreach ($matches[0] as $index => $cell) + { + $cell = trim($cell); + + $Element = array( + 'name' => 'td', + 'handler' => 'line', + 'text' => $cell, + ); + + if (isset($Block['alignments'][$index])) + { + $Element['attributes'] = array( + 'style' => 'text-align: '.$Block['alignments'][$index].';', + ); + } + + $Elements []= $Element; + } + + $Element = array( + 'name' => 'tr', + 'handler' => 'elements', + 'text' => $Elements, + ); + + $Block['element']['text'][1]['text'] []= $Element; + + return $Block; + } + } + + # + # ~ + # + + protected function paragraph($Line) + { + $Block = array( + 'element' => array( + 'name' => 'p', + 'text' => $Line['text'], + 'handler' => 'line', + ), + ); + + return $Block; + } + + # + # Inline Elements + # + + protected $InlineTypes = array( + '"' => array('SpecialCharacter'), + '!' => array('Image'), + '&' => array('SpecialCharacter'), + '*' => array('Emphasis'), + ':' => array('Url'), + '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'), + '>' => array('SpecialCharacter'), + '[' => array('Link'), + '_' => array('Emphasis'), + '`' => array('Code'), + '~' => array('Strikethrough'), + '\\' => array('EscapeSequence'), + ); + + # ~ + + protected $inlineMarkerList = '!"*_&[:<>`~\\'; + + # + # ~ + # + + public function line($text) + { + $markup = ''; + + # $excerpt is based on the first occurrence of a marker + + while ($excerpt = strpbrk($text, $this->inlineMarkerList)) + { + $marker = $excerpt[0]; + + $markerPosition = strpos($text, $marker); + + $Excerpt = array('text' => $excerpt, 'context' => $text); + + foreach ($this->InlineTypes[$marker] as $inlineType) + { + $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; + } + + # the text that comes before the inline + $unmarkedText = substr($text, 0, $Inline['position']); + + # compile the unmarked text + $markup .= $this->unmarkedText($unmarkedText); + + # compile the inline + $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']); + + # 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); + + $markup .= $this->unmarkedText($unmarkedText); + + $text = substr($text, $markerPosition + 1); + } + + $markup .= $this->unmarkedText($text); + + return $markup; + } + + # + # ~ + # + + 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) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/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' => 'line', + 'text' => $matches[1], + ), + ); + } + + protected function inlineEscapeSequence($Excerpt) + { + if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) + { + return array( + 'markup' => $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']['text'], + ), + ), + ); + + $Inline['element']['attributes'] += $Link['element']['attributes']; + + unset($Inline['element']['attributes']['href']); + + return $Inline; + } + + protected function inlineLink($Excerpt) + { + $Element = array( + 'name' => 'a', + 'handler' => 'line', + 'text' => null, + 'attributes' => array( + 'href' => null, + 'title' => null, + ), + ); + + $extent = 0; + + $remainder = $Excerpt['text']; + + if (preg_match('/\[((?:[^][]|(?R))*)\]/', $remainder, $matches)) + { + $Element['text'] = $matches[1]; + + $extent += strlen($matches[0]); + + $remainder = substr($remainder, $extent); + } + else + { + return; + } + + if (preg_match('/^[(]((?:[^ ()]|[(][^ )]+[)])+)(?:[ ]+("[^"]*"|\'[^\']*\'))?[)]/', $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['text']; + $definition = strtolower($definition); + + $extent += strlen($matches[0]); + } + else + { + $definition = strtolower($Element['text']); + } + + if ( ! isset($this->DefinitionData['Reference'][$definition])) + { + return; + } + + $Definition = $this->DefinitionData['Reference'][$definition]; + + $Element['attributes']['href'] = $Definition['url']; + $Element['attributes']['title'] = $Definition['title']; + } + + $Element['attributes']['href'] = str_replace(array('&', '<'), array('&', '<'), $Element['attributes']['href']); + + return array( + 'extent' => $extent, + 'element' => $Element, + ); + } + + protected function inlineMarkup($Excerpt) + { + if ($this->markupEscaped or strpos($Excerpt['text'], '>') === false) + { + return; + } + + if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w*[ ]*>/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + } + + protected function inlineSpecialCharacter($Excerpt) + { + if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text'])) + { + return array( + 'markup' => '&', + 'extent' => 1, + ); + } + + $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot'); + + if (isset($SpecialCharacter[$Excerpt['text'][0]])) + { + return array( + 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';', + 'extent' => 1, + ); + } + } + + 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', + 'text' => $matches[1], + 'handler' => 'line', + ), + ); + } + } + + protected function inlineUrl($Excerpt) + { + if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') + { + return; + } + + if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) + { + $Inline = array( + 'extent' => strlen($matches[0][0]), + 'position' => $matches[0][1], + 'element' => array( + 'name' => 'a', + 'text' => $matches[0][0], + 'attributes' => array( + 'href' => $matches[0][0], + ), + ), + ); + + return $Inline; + } + } + + protected function inlineUrlTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) + { + $url = str_replace(array('&', '<'), array('&', '<'), $matches[1]); + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + # ~ + + protected function unmarkedText($text) + { + if ($this->breaksEnabled) + { + $text = preg_replace('/[ ]*\n/', "
\n", $text); + } + else + { + $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "
\n", $text); + $text = str_replace(" \n", "\n", $text); + } + + return $text; + } + + # + # Handlers + # + + protected function element(array $Element) + { + $markup = '<'.$Element['name']; + + if (isset($Element['attributes'])) + { + foreach ($Element['attributes'] as $name => $value) + { + if ($value === null) + { + continue; + } + + $markup .= ' '.$name.'="'.$value.'"'; + } + } + + if (isset($Element['text'])) + { + $markup .= '>'; + + if (isset($Element['handler'])) + { + $markup .= $this->{$Element['handler']}($Element['text']); + } + else + { + $markup .= $Element['text']; + } + + $markup .= ''; + } + else + { + $markup .= ' />'; + } + + return $markup; + } + + protected function elements(array $Elements) + { + $markup = ''; + + foreach ($Elements as $Element) + { + $markup .= "\n" . $this->element($Element); + } + + $markup .= "\n"; + + return $markup; + } + + # ~ + + protected function li($lines) + { + $markup = $this->lines($lines); + + $trimmedMarkup = trim($markup); + + if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '

') + { + $markup = $trimmedMarkup; + $markup = substr($markup, 3); + + $position = strpos($markup, "

"); + + $markup = substr_replace($markup, '', $position, 4); + } + + return $markup; + } + + # + # Deprecated Methods + # + + function parse($text) + { + $markup = $this->text($text); + + return $markup; + } + + # + # Static Methods + # + + 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', 'sub', 'mark', + 'u', 'xm', 'sup', 'nobr', + 'var', 'ruby', + 'wbr', 'span', + 'time', + ); +} diff --git a/kirby/vendors/parsedownextra.php b/kirby/vendors/parsedownextra.php new file mode 100644 index 0000000..be6966d --- /dev/null +++ b/kirby/vendors/parsedownextra.php @@ -0,0 +1,526 @@ +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'); + } + + # + # ~ + + function text($text) + { + $markup = parent::text($text); + + # 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 isset($Block['type'])) + { + return; + } + + $Element = array( + 'name' => 'dl', + 'handler' => 'elements', + 'text' => array(), + ); + + $terms = explode("\n", $Block['element']['text']); + + foreach ($terms as $term) + { + $Element['text'] []= array( + 'name' => 'dt', + 'handler' => 'line', + 'text' => $term, + ); + } + + $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'] = 'text'; + $Block['dd']['text'] .= "\n\n"; + + unset($Block['interrupted']); + } + + $text = substr($Line['body'], min($Line['indent'], 4)); + + $Block['dd']['text'] .= "\n" . $text; + + return $Block; + } + } + + # + # Header + + protected function blockHeader($Line) + { + $Block = parent::blockHeader($Line); + + if (preg_match('/[ #]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['text'], $matches, PREG_OFFSET_CAPTURE)) + { + $attributeString = $matches[1][0]; + + $Block['element']['attributes'] = $this->parseAttributeData($attributeString); + + $Block['element']['text'] = substr($Block['element']['text'], 0, $matches[0][1]); + } + + return $Block; + } + + # + # Markup + + protected function blockMarkupComplete($Block) + { + if ( ! isset($Block['void'])) + { + $Block['markup'] = $this->processTag($Block['markup']); + } + + return $Block; + } + + # + # Setext + + protected function blockSetextHeader($Line, array $Block = null) + { + $Block = parent::blockSetextHeader($Line, $Block); + + if (preg_match('/[ ]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['text'], $matches, PREG_OFFSET_CAPTURE)) + { + $attributeString = $matches[1][0]; + + $Block['element']['attributes'] = $this->parseAttributeData($attributeString); + + $Block['element']['text'] = substr($Block['element']['text'], 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), + 'handler' => 'element', + 'text' => 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; + } + + # + # ~ + # + + protected function unmarkedText($text) + { + $text = parent::unmarkedText($text); + + if (isset($this->DefinitionData['Abbreviation'])) + { + foreach ($this->DefinitionData['Abbreviation'] as $abbreviation => $meaning) + { + $pattern = '/\b'.preg_quote($abbreviation, '/').'\b/'; + + $text = preg_replace($pattern, ''.$abbreviation.'', $text); + } + } + + return $text; + } + + # + # 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' => 'line', + 'text' => $text, + ); + + if (isset($Block['interrupted'])) + { + $Block['dd']['handler'] = 'text'; + + unset($Block['interrupted']); + } + + $Block['element']['text'] []= & $Block['dd']; + + return $Block; + } + + protected function buildFootnoteElement() + { + $Element = array( + 'name' => 'div', + 'attributes' => array('class' => 'footnotes'), + 'handler' => 'elements', + 'text' => array( + array( + 'name' => 'hr', + ), + array( + 'name' => 'ol', + 'handler' => 'elements', + 'text' => array(), + ), + ), + ); + + uasort($this->DefinitionData['Footnote'], 'self::sortFootnotes'); + + foreach ($this->DefinitionData['Footnote'] as $definitionId => $DefinitionData) + { + if ( ! isset($DefinitionData['number'])) + { + continue; + } + + $text = $DefinitionData['text']; + + $text = parent::text($text); + + $numbers = range(1, $DefinitionData['count']); + + $backLinksMarkup = ''; + + foreach ($numbers as $number) + { + $backLinksMarkup .= ' '; + } + + $backLinksMarkup = substr($backLinksMarkup, 1); + + if (substr($text, - 4) === '

') + { + $backLinksMarkup = ' '.$backLinksMarkup; + + $text = substr_replace($text, $backLinksMarkup.'

', - 4); + } + else + { + $text .= "\n".'

'.$backLinksMarkup.'

'; + } + + $Element['text'][1]['text'] []= array( + 'name' => 'li', + 'attributes' => array('id' => 'fn:'.$definitionId), + 'text' => "\n".$text."\n", + ); + } + + 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/vendors/smartypants.php b/kirby/vendors/smartypants.php new file mode 100755 index 0000000..1f73456 --- /dev/null +++ b/kirby/vendors/smartypants.php @@ -0,0 +1,1094 @@ + +# +# Original SmartyPants +# Copyright (c) 2003-2004 John Gruber +# +# + + +define( 'SMARTYPANTS_VERSION', "1.5.1f" ); # Sun 23 Jan 2013 +define( 'SMARTYPANTSTYPOGRAPHER_VERSION', "1.0.1" ); # Sun 23 Jan 2013 + +# +# Default configuration: +# +# 1 -> "--" for em-dashes; no en-dash support +# 2 -> "---" for em-dashes; "--" for en-dashes +# 3 -> "--" for em-dashes; "---" for en-dashes +# See docs for more configuration options. +# +define( 'SMARTYPANTS_ATTR', c::get('smartypants.attr', 1) ); + +# Openning and closing smart double-quotes. +define( 'SMARTYPANTS_SMART_DOUBLEQUOTE_OPEN', c::get('smartypants.doublequote.open', '“') ); +define( 'SMARTYPANTS_SMART_DOUBLEQUOTE_CLOSE', c::get('smartypants.doublequote.close', '”') ); + +# Space around em-dashes. "He_—_or she_—_should change that." +define( 'SMARTYPANTS_SPACE_EMDASH', c::get('smartypants.space.emdash', ' ') ); + +# Space around en-dashes. "He_–_or she_–_should change that." +define( 'SMARTYPANTS_SPACE_ENDASH', c::get('smartypants.space.endash', ' ') ); + +# Space before a colon. "He said_: here it is." +define( 'SMARTYPANTS_SPACE_COLON', c::get('smartypants.space.colon', ' ') ); + +# Space before a semicolon. "That's what I said_; that's what he said." +define( 'SMARTYPANTS_SPACE_SEMICOLON', c::get('smartypants.space.semicolon', ' ') ); + +# Space before a question mark and an exclamation mark: "¡_Holà_! What_?" +define( 'SMARTYPANTS_SPACE_MARKS', c::get('smartypants.space.marks', ' ') ); + +# Space inside french quotes. "Voici la «_chose_» qui m'a attaqué." +define( 'SMARTYPANTS_SPACE_FRENCHQUOTE', c::get('smartypants.space.frenchquote', ' ') ); + +# Space as thousand separator. "On compte 10_000 maisons sur cette liste." +define( 'SMARTYPANTS_SPACE_THOUSAND', c::get('smartypants.space.thousand', ' ') ); + +# Space before a unit abreviation. "This 12_kg of matter costs 10_$." +define( 'SMARTYPANTS_SPACE_UNIT', c::get('smartypants.space.unit', ' ') ); + +# SmartyPants will not alter the content of these tags: +define( 'SMARTYPANTS_TAGS_TO_SKIP', c::get('smartypants.skip', 'pre|code|kbd|script|style|math') ); + + +# +# SmartyPants Parser Class +# + +class SmartyPants_Parser { + + # Options to specify which transformations to make: + var $do_nothing = 0; + var $do_quotes = 0; + var $do_backticks = 0; + var $do_dashes = 0; + var $do_ellipses = 0; + var $do_stupefy = 0; + var $convert_quot = 0; # should we translate " entities into normal quotes? + + function __construct($attr = SMARTYPANTS_ATTR) { + # + # Initialize a SmartyPants_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. + } + } + } + } + + 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('@<(/?)(?:'.SMARTYPANTS_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 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 = "’";
+				}
+				else {
+					$t = "‘";
+				}
+			}
+			else if ($t == '"') {
+				# Special case: single-character " token
+				if (preg_match('/\S/', $prev_token_last_char)) {
+					$t = "”";
+				}
+				else {
+					$t = "“";
+				}
+			}
+			else {
+				# Normal case:
+				$t = $this->educateQuotes($t);
+			}
+		}
+
+		if ($this->do_stupefy) $t = $this->stupefyEntities($t);
+		
+		return $t;
+	}
+
+
+	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?”
+	#
+		# 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('’',                 '”'), $_);
+
+
+		# Special case for double sets of quotes, e.g.:
+		#   

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

+ $_ = preg_replace( + array("/\"'(?=\w)/", "/'\"(?=\w)/"), + array('“‘', '‘“'), $_); + + # Special case for decade abbreviations (the '80s): + $_ = preg_replace("/'(?=\\d{2}s)/", '’', $_); + + $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‘', $_); + # 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’', $_); + + # Any remaining single quotes should be opening ones: + $_ = str_replace("'", '‘', $_); + + + # 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“', $_); + + # 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”', $_); + + # Any remaining quotes should be opening ones. + $_ = str_replace('"', '“', $_); + + return $_; + } + + + 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('“', '”'), $_); + return $_; + } + + + 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('‘', '’'), $_); + return $_; + } + + + function educateDashes($_) { + # + # Parameter: String. + # + # Returns: The string, with each instance of "--" translated to + # an em-dash HTML entity. + # + + $_ = str_replace('--', '—', $_); + return $_; + } + + + 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('—', '–'), $_); + return $_; + } + + + 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('–', '—'), $_); + return $_; + } + + + 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("...", ". . .",), '…', $_); + return $_; + } + + + 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 $_; + } + + + 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 $_; + } + + + 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; + } + +} + + +# +# SmartyPants Typographer Parser Class +# +class SmartyPantsTypographer_Parser extends SmartyPants_Parser { + + # Options to specify which transformations to make: + var $do_comma_quotes = 0; + var $do_guillemets = 0; + var $do_space_emdash = 0; + var $do_space_endash = 0; + var $do_space_colon = 0; + var $do_space_semicolon = 0; + var $do_space_marks = 0; + var $do_space_frenchquote = 0; + var $do_space_thousand = 0; + var $do_space_unit = 0; + + # Smart quote characters: + var $smart_doublequote_open = SMARTYPANTS_SMART_DOUBLEQUOTE_OPEN; + var $smart_doublequote_close = SMARTYPANTS_SMART_DOUBLEQUOTE_CLOSE; + var $smart_singlequote_open = '‘'; + var $smart_singlequote_close = '’'; # Also apostrophe. + + # Space characters for different places: + var $space_emdash = SMARTYPANTS_SPACE_EMDASH; + var $space_endash = SMARTYPANTS_SPACE_ENDASH; + var $space_colon = SMARTYPANTS_SPACE_COLON; + var $space_semicolon = SMARTYPANTS_SPACE_SEMICOLON; + var $space_marks = SMARTYPANTS_SPACE_MARKS; + var $space_frenchquote = SMARTYPANTS_SPACE_FRENCHQUOTE; + var $space_thousand = SMARTYPANTS_SPACE_THOUSAND; + var $space_unit = SMARTYPANTS_SPACE_UNIT; + + # Expression of a space (breakable or not): + var $space = '(?: | | |�*160;|�*[aA]0;)'; + + + + function __construct($attr = SMARTYPANTS_ATTR) { + # + # 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_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 == ":") { $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 educate($t, $prev_token_last_char) { + $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; + } + + + 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 $_; + } + + + 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(",,", '„', $_); + return $_; + } + + + 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}/", '«', $_); + $_ = preg_replace("/(?:>|>){2}/", '»', $_); + return $_; + } + + + 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 $_; + } + + + 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 $_; + } + + + 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 $_; + } + + + 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 $_; + } + + + 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 $_; + } + + + 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 $_; + } + + + 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 $_; + } + + + var $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 + + 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 $_; + } + + + 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 $_; + } + + + function stupefyEntities($_) { + # + # Adding angle quotes and lower quotes to SmartyPants's stupefy mode. + # + $_ = parent::stupefyEntities($_); + + $_ = str_replace(array('„', '«', '»'), '"', $_); + + return $_; + } + + + function processEscapes($_) { + # + # Adding a few more escapes to SmartyPants's escapes: + # + # Escape Value + # ------ ----- + # \, , + # \< < + # \> > + # + $_ = parent::processEscapes($_); + + $_ = str_replace( + array('\,', '\<', '\>', '\<', '\>'), + array(',', '<', '>', '<', '>'), $_); + + return $_; + } +} + + +/* + +PHP SmartyPants Typographer +=========================== + +Version History +--------------- + +1.0 (28 Jun 2006) + +* First public release of PHP SmartyPants Typographer. + + +Bugs +---- + +To file bug reports or feature requests (other than topics listed in the +Caveats section above) please send email to: + + + +If the bug involves quotes being curled the wrong way, please send example +text to illustrate. + + +### Algorithmic Shortcomings ### + +One situation in which quotes will get curled the wrong way is when +apostrophes are used at the start of leading contractions. For example: + + 'Twas the night before Christmas. + +In the case above, SmartyPants will turn the apostrophe into an opening +single-quote, when in fact it should be a closing one. I don't think +this problem can be solved in the general case -- every word processor +I've tried gets this wrong as well. In such cases, it's best to use the +proper HTML entity for closing single-quotes (`’`) by hand. + + +Copyright and License +--------------------- + +PHP SmartyPants & Typographer +Copyright (c) 2004-2006 Michel Fortin + +All rights reserved. + +Original SmartyPants +Copyright (c) 2003-2004 John Gruber + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name "SmartyPants" nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +This software is provided by the copyright holders and contributors "as is" +and any express or implied warranties, including, but not limited to, the +implied warranties of merchantability and fitness for a particular purpose +are disclaimed. In no event shall the copyright owner or contributors be +liable for any direct, indirect, incidental, special, exemplary, or +consequential damages (including, but not limited to, procurement of +substitute goods or services; loss of use, data, or profits; or business +interruption) however caused and on any theory of liability, whether in +contract, strict liability, or tort (including negligence or otherwise) +arising in any way out of the use of this software, even if advised of the +possibility of such damage. + +*/ +?> \ No newline at end of file diff --git a/panel b/panel deleted file mode 160000 index 53ebd4b..0000000 --- a/panel +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 53ebd4b6964c02aab5671d2735f59ccc0178f144 diff --git a/panel/app/bootstrap.php b/panel/app/bootstrap.php new file mode 100644 index 0000000..2b73833 --- /dev/null +++ b/panel/app/bootstrap.php @@ -0,0 +1,71 @@ + 'panel.php', + + // global stuff + 'kirby\\panel\\login' => 'panel' . DS . 'login.php', + 'kirby\\panel\\translation' => 'panel' . DS . 'translation.php', + 'kirby\\panel\\autocomplete' => 'panel' . DS . 'autocomplete.php', + 'kirby\\panel\\roots' => 'panel' . DS . 'roots.php', + 'kirby\\panel\\urls' => 'panel' . DS . 'urls.php', + 'kirby\\panel\\view' => 'panel' . DS . 'view.php', + 'kirby\\panel\\layout' => 'panel' . DS . 'layout.php', + 'kirby\\panel\\snippet' => 'panel' . DS . 'snippet.php', + 'kirby\\panel\\installer' => 'panel' . DS . 'installer.php', + 'kirby\\panel\\widgets' => 'panel' . DS . 'widgets.php', + 'kirby\\panel\\topbar' => 'panel' . DS . 'topbar.php', + 'kirby\\panel\\search' => 'panel' . DS . 'search.php', + 'kirby\\panel\\structure' => 'panel' . DS . 'structure.php', + 'kirby\\panel\\structure\\store' => 'panel' . DS . 'structure' . DS . 'store.php', + 'kirby\\panel\\upload' => 'panel' . DS . 'upload.php', + + // controllers + 'kirby\\panel\\controllers\\base' => 'panel' . DS . 'controllers' . DS . 'base.php', + 'kirby\\panel\\controllers\\field' => 'panel' . DS . 'controllers' . DS . 'field.php', + + // form + 'kirby\\panel\\form' => 'panel' . DS . 'form.php', + 'kirby\\panel\\form\\plugins' => 'panel' . DS . 'form' . DS . 'plugins.php', + 'kirby\\panel\\form\\fieldoptions' => 'panel' . DS . 'form' . DS . 'fieldoptions.php', + + // models + 'kirby\\panel\\models\\site' => 'panel' . DS . 'models' . DS . 'site.php', + 'kirby\\panel\\models\\page' => 'panel' . DS . 'models' . DS . 'page.php', + 'kirby\\panel\\models\\page\\addbutton' => 'panel' . DS . 'models' . DS . 'page' . DS . 'addbutton.php', + 'kirby\\panel\\models\\page\\menu' => 'panel' . DS . 'models' . DS . 'page' . DS . 'menu.php', + 'kirby\\panel\\models\\page\\sidebar' => 'panel' . DS . 'models' . DS . 'page' . DS . 'sidebar.php', + 'kirby\\panel\\models\\page\\changes' => 'panel' . DS . 'models' . DS . 'page' . DS . 'changes.php', + 'kirby\\panel\\models\\page\\uploader' => 'panel' . DS . 'models' . DS . 'page' . DS . 'uploader.php', + 'kirby\\panel\\models\\page\\sorter' => 'panel' . DS . 'models' . DS . 'page' . DS . 'sorter.php', + 'kirby\\panel\\models\\page\\blueprint' => 'panel' . DS . 'models' . DS . 'page' . DS . 'blueprint.php', + 'kirby\\panel\\models\\page\\blueprint\\pages' => 'panel' . DS . 'models' . DS . 'page' . DS . 'blueprint' . DS . 'pages.php', + 'kirby\\panel\\models\\page\\blueprint\\files' => 'panel' . DS . 'models' . DS . 'page' . DS . 'blueprint' . DS . 'files.php', + 'kirby\\panel\\models\\page\\blueprint\\fields' => 'panel' . DS . 'models' . DS . 'page' . DS . 'blueprint' . DS . 'fields.php', + 'kirby\\panel\\models\\page\\blueprint\\field' => 'panel' . DS . 'models' . DS . 'page' . DS . 'blueprint' . DS . 'field.php', + 'kirby\\panel\\models\\page\\blueprint\\options' => 'panel' . DS . 'models' . DS . 'page' . DS . 'blueprint' . DS . 'options.php', + 'kirby\\panel\\models\\file' => 'panel' . DS . 'models' . DS . 'file.php', + 'kirby\\panel\\models\\file\\menu' => 'panel' . DS . 'models' . DS . 'file' . DS . 'menu.php', + 'kirby\\panel\\models\\user' => 'panel' . DS . 'models' . DS . 'user.php', + 'kirby\\panel\\models\\user\\blueprint' => 'panel' . DS . 'models' . DS . 'user' . DS . 'blueprint.php', + 'kirby\\panel\\models\\user\\avatar' => 'panel' . DS . 'models' . DS . 'user' . DS . 'avatar.php', + 'kirby\\panel\\models\\user\\history' => 'panel' . DS . 'models' . DS . 'user' . DS . 'history.php', + + // collections + 'kirby\\panel\\collections\\users' => 'panel' . DS . 'collections' . DS . 'users.php', + 'kirby\\panel\\collections\\files' => 'panel' . DS . 'collections' . DS . 'files.php', + 'kirby\\panel\\collections\\children' => 'panel' . DS . 'collections' . DS . 'children.php', + +), __DIR__ . DS . 'src'); + + +// some fallbacks for possible namespace issues and convenience +class_alias('Kirby\\Panel\\Form\\FieldOptions', 'FieldOptions'); +class_alias('Kirby\\Panel', 'Panel'); + +include(__DIR__ . DS . 'helpers.php'); + diff --git a/panel/app/config/routes.php b/panel/app/config/routes.php new file mode 100644 index 0000000..6607eef --- /dev/null +++ b/panel/app/config/routes.php @@ -0,0 +1,298 @@ + 'login', + 'action' => 'AuthController::login', + 'filter' => 'isInstalled', + 'method' => 'GET|POST' + ), + array( + 'pattern' => 'logout', + 'action' => 'AuthController::logout', + 'method' => 'GET', + 'filter' => 'auth', + ), + + // Installation + array( + 'pattern' => 'install', + 'action' => 'InstallationController::index', + 'method' => 'GET|POST' + ), + + // Dashboard + array( + 'pattern' => '/', + 'action' => 'DashboardController::index', + 'filter' => array('auth', 'isInstalled'), + ), + + // Search + array( + 'pattern' => 'search', + 'action' => 'SearchController::results', + 'method' => 'GET|POST', + 'filter' => array('auth'), + ), + + // Options + array( + 'pattern' => 'options', + 'action' => 'OptionsController::index', + 'method' => 'GET|POST', + 'filter' => 'auth' + ), + + // Files + array( + 'pattern' => array( + 'site(/)file/(:any)/edit', + 'pages/(:all)/file/(:any)/edit', + ), + 'action' => 'FilesController::edit', + 'filter' => 'auth', + 'method' => 'POST|GET', + ), + array( + 'pattern' => array( + 'site(/)file/(:any)/context', + 'pages/(:all)/file/(:any)/context', + ), + 'action' => 'FilesController::context', + 'filter' => 'auth', + 'method' => 'GET', + ), + array( + 'pattern' => array( + 'site(/)file/(:any)/thumb', + 'pages/(:all)/file/(:any)/thumb', + ), + 'action' => 'FilesController::thumb', + 'filter' => 'auth', + 'method' => 'GET', + ), + array( + 'pattern' => array( + 'site(/)file/(:any)/delete', + 'pages/(:all)/file/(:any)/delete', + ), + 'action' => 'FilesController::delete', + 'filter' => 'auth', + 'method' => 'POST|GET', + ), + array( + 'pattern' => array( + 'site(/)file/(:any)/replace', + 'pages/(:all)/file/(:any)/replace', + ), + 'action' => 'FilesController::replace', + 'filter' => 'auth', + 'method' => 'POST', + ), + array( + 'pattern' => array( + 'site(/)files', + 'pages/(:all)/files', + ), + 'action' => 'FilesController::index', + 'filter' => 'auth', + 'method' => 'POST|GET', + ), + + // Field routes + array( + 'pattern' => array( + 'site(/)file/(:any)/field/(:any)/(:any)/(:all)', + 'pages/(:all)/file/(:any)/field/(:any)/(:any)/(:all)', + ), + 'action' => 'FieldController::forFile', + 'filter' => 'auth', + 'method' => 'GET|POST' + ), + array( + 'pattern' => array( + 'site(/)field/(:any)/(:any)/(:all)', + 'pages/(:all)/field/(:any)/(:any)/(:all)', + ), + 'action' => 'FieldController::forPage', + 'filter' => 'auth', + 'method' => 'GET|POST' + ), + array( + 'pattern' => array( + 'users/(:all)/field/(:any)/(:any)/(:all)', + ), + 'action' => 'FieldController::forUser', + 'filter' => 'auth', + 'method' => 'GET|POST' + ), + + // New Page + array( + 'pattern' => array( + 'site(/)add', + 'pages/(:all)/add', + ), + 'action' => 'PagesController::add', + 'filter' => 'auth', + 'method' => 'POST|GET' + ), + + // URL Settings + array( + 'pattern' => 'pages/(:all)/url', + 'action' => 'PagesController::url', + 'filter' => 'auth', + 'method' => 'POST|GET' + ), + + // Template Modal + array( + 'pattern' => 'pages/(:all)/template', + 'action' => 'PagesController::template', + 'filter' => 'auth', + 'method' => 'POST|GET' + ), + + // Toggle visibility + array( + 'pattern' => 'pages/(:all)/toggle', + 'action' => 'PagesController::toggle', + 'filter' => 'auth', + 'method' => 'POST|GET' + ), + + // Delete a page + array( + 'pattern' => 'pages/(:all)/delete', + 'action' => 'PagesController::delete', + 'filter' => 'auth', + 'method' => 'POST|GET' + ), + + // Keeping page changes + array( + 'pattern' => array( + 'site(/)keep', + 'pages/(:all)/keep', + ), + 'action' => 'PagesController::keep', + 'method' => 'GET|POST', + 'filter' => 'auth', + ), + + // Discarding page changes + array( + 'pattern' => array( + 'site(/)discard', + 'pages/(:all)/discard', + ), + 'action' => 'PagesController::discard', + 'method' => 'GET|POST', + 'filter' => 'auth', + ), + + // Page context menu + array( + 'pattern' => 'pages/(:all)/context', + 'action' => 'PagesController::context', + 'method' => 'GET', + 'filter' => 'auth', + ), + + // Upload a file + array( + 'pattern' => array( + 'site(/)upload', + 'pages/(:all)/upload', + ), + 'action' => 'FilesController::upload', + 'filter' => 'auth', + 'method' => 'POST' + ), + + // Subpages + array( + 'pattern' => array( + 'site(/)subpages', + 'pages/(:all)/subpages', + ), + 'action' => 'SubpagesController::index', + 'filter' => 'auth', + 'method' => 'POST|GET' + ), + + // Page + array( + 'pattern' => 'pages/(:all)/edit', + 'action' => 'PagesController::edit', + 'filter' => 'auth', + 'method' => 'POST|GET' + ), + + // Users + array( + 'pattern' => 'users', + 'action' => 'UsersController::index', + 'filter' => 'auth' + ), + array( + 'pattern' => 'users/add', + 'action' => 'UsersController::add', + 'filter' => 'auth', + 'method' => 'POST|GET' + ), + array( + 'pattern' => 'users/(:any)/edit', + 'action' => 'UsersController::edit', + 'filter' => 'auth', + 'method' => 'POST|GET' + ), + array( + 'pattern' => 'users/(:any)/delete', + 'action' => 'UsersController::delete', + 'filter' => 'auth', + 'method' => 'POST|GET' + ), + + // Avatars + array( + 'pattern' => 'users/(:any)/avatar', + 'action' => 'AvatarsController::upload', + 'filter' => 'auth', + 'method' => 'POST' + ), + array( + 'pattern' => 'users/(:any)/avatar/delete', + 'action' => 'AvatarsController::delete', + 'filter' => 'auth', + 'method' => 'POST|GET' + ), + + // Autocomplete + array( + 'pattern' => 'api/autocomplete/(:any)', + 'action' => 'AutocompleteController::index', + 'method' => 'POST', + 'filter' => 'auth', + ), + + // form assets + array( + 'pattern' => 'plugins/js', + 'action' => 'AssetsController::js', + 'method' => 'GET', + 'filter' => 'auth' + ), + array( + 'pattern' => 'plugins/css', + 'action' => 'AssetsController::css', + 'method' => 'GET', + 'filter' => 'auth' + ), + +); \ No newline at end of file diff --git a/panel/app/controllers/assets.php b/panel/app/controllers/assets.php new file mode 100644 index 0000000..705356f --- /dev/null +++ b/panel/app/controllers/assets.php @@ -0,0 +1,17 @@ +plugins()->js(), 'text/javascript'); + } + + public function css() { + $form = new Form(); + return new Response($form->plugins()->css(), 'text/css'); + } + +} \ No newline at end of file diff --git a/panel/app/controllers/auth.php b/panel/app/controllers/auth.php new file mode 100644 index 0000000..5e0f399 --- /dev/null +++ b/panel/app/controllers/auth.php @@ -0,0 +1,62 @@ +layout('base', array( + 'content' => $this->view('auth/error', array( + 'error' => $e->getMessage() + )) + )); + } + + if($login->isAuthenticated()) { + $this->redirect(); + } + + if($login->isBlocked()) { + return $this->layout('base', array( + 'content' => $this->view('auth/block') + )); + } + + $self = $this; + $form = $this->form('auth/login', null, function($form) use($self, $login) { + + $data = $form->serialize(); + + try { + $login->attempt($data['username'], $data['password']); + $self->redirect(); + } catch(Exception $e) { + $form->alert(l('login.error')); + $form->fields->username->error = true; + $form->fields->password->error = true; + } + + }); + + return $this->layout('base', array( + 'bodyclass' => 'login', + 'content' => $this->view('auth/login', compact('form')) + )); + + } + + public function logout() { + + if($user = panel()->user()) { + $user->logout(); + } + + $this->redirect('login'); + + } + +} \ No newline at end of file diff --git a/panel/app/controllers/autocomplete.php b/panel/app/controllers/autocomplete.php new file mode 100644 index 0000000..6ec6b84 --- /dev/null +++ b/panel/app/controllers/autocomplete.php @@ -0,0 +1,20 @@ +result(); + } catch(Exception $e) { + $result = array(); + } + + return $this->json(array( + 'data' => $result + )); + + } + +} \ No newline at end of file diff --git a/panel/app/controllers/avatars.php b/panel/app/controllers/avatars.php new file mode 100644 index 0000000..c82c2bf --- /dev/null +++ b/panel/app/controllers/avatars.php @@ -0,0 +1,49 @@ +user($username); + + try { + $user->avatar()->upload(); + $this->notify(':)'); + } catch(Exception $e) { + $this->alert($e->getMessage()); + } + + $this->redirect($user); + + } + + public function delete($username) { + + $self = $this; + $user = $this->user($username); + $avatar = $user->avatar(); + + if(!$avatar->exists()) { + return $this->modal('error', array( + 'text' => l('users.avatar.missing'), + 'back' => $user->url() + )); + } + + $form = $avatar->form('delete', function($form) use($user, $avatar, $self) { + + try { + $avatar->delete(); + $self->notify(':)'); + $self->redirect($user); + } catch(Exception $e) { + $form->alert($e->getMessage()); + } + + }); + + return $this->modal('avatars/delete', compact('form')); + + } + +} \ No newline at end of file diff --git a/panel/app/controllers/dashboard.php b/panel/app/controllers/dashboard.php new file mode 100644 index 0000000..5f3f843 --- /dev/null +++ b/panel/app/controllers/dashboard.php @@ -0,0 +1,13 @@ +screen('dashboard/index', panel()->site(), array( + 'widgets' => new Kirby\Panel\Widgets() + )); + + } + +} \ No newline at end of file diff --git a/panel/app/controllers/error.php b/panel/app/controllers/error.php new file mode 100644 index 0000000..cf6ee6f --- /dev/null +++ b/panel/app/controllers/error.php @@ -0,0 +1,35 @@ +auth(); + + if(is_null($text)) { + $text = l('pages.error.missing'); + } + + if(server::get('HTTP_MODAL')) { + return $this->modal('error', array( + 'text' => $text, + 'back' => url::last(), + )); + } else { + return $this->screen('error/index', 'error', array( + 'text' => $text, + 'exception' => $exception + )); + } + + } + + public function auth() { + try { + $user = panel()->user(); + } catch(Exception $e) { + $this->redirect('login'); + } + } + +} \ No newline at end of file diff --git a/panel/app/controllers/field.php b/panel/app/controllers/field.php new file mode 100644 index 0000000..ae571c3 --- /dev/null +++ b/panel/app/controllers/field.php @@ -0,0 +1,82 @@ +page($pageId); + $file = $page->file(File::decodeFilename($filename)); + + if(!$file) { + throw new Exception(l('files.error.missing.file')); + } + + $form = $file->form('edit', function() {}); + + return $this->route($file, $form, $fieldName, $fieldType, $path); + + } + + public function forPage($pageId, $fieldName, $fieldType, $path) { + + $page = $this->page($pageId); + $form = $page->form('edit', function() {}); + + return $this->route($page, $form, $fieldName, $fieldType, $path); + + } + + public function forUser($username, $fieldName, $fieldType, $path) { + + $user = panel()->user($username); + $form = $user->form('user', function() {}); + + return $this->route($user, $form, $fieldName, $fieldType, $path); + + } + + public function route($model, $form, $fieldName, $fieldType, $path) { + + $field = $form->fields()->$fieldName; + + if(!$field or $field->type() !== $fieldType) { + throw new Exception('Invalid field'); + } + + $routes = $field->routes(); + $router = new Router($routes); + + if($route = $router->run($path)) { + + if(is_callable($route->action()) and is_a($route->action(), 'Closure')) { + return call($route->action(), $route->arguments()); + } else { + + $controllerFile = $field->root() . DS . 'controller.php'; + $controllerName = $fieldType . 'FieldController'; + + if(!file_exists($controllerFile)) { + throw new Exception(l('fields.error.missing.controller')); + } + + require_once($controllerFile); + + if(!class_exists($controllerName)) { + throw new Exception(l('fields.error.missing.class')); + } + + $controller = new $controllerName($model, $field); + + return call(array($controller, $route->action()), $route->arguments()); + + } + + } else { + throw new Exception(l('fields.error.route.invalid')); + } + + } + +} \ No newline at end of file diff --git a/panel/app/controllers/files.php b/panel/app/controllers/files.php new file mode 100644 index 0000000..3740234 --- /dev/null +++ b/panel/app/controllers/files.php @@ -0,0 +1,195 @@ +page($id); + $files = $page->files(); + + // don't create the view if the page is not allowed to have files + if(!$page->canHaveFiles()) { + throw new Exception(l('files.index.error.disabled')); + } + + // sort action + $this->sort($page); + + return $this->screen('files/index', $files, array( + 'page' => $page, + 'files' => $files, + 'back' => $page->url('edit'), + 'sortable' => $page->canSortFiles(), + 'uploader' => $this->snippet('uploader', array('url' => $page->url('upload'))) + )); + + } + + public function edit($id, $filename) { + + $self = $this; + $page = $this->page($id); + + try { + $file = $this->file($page, $filename); + } catch(Exception $e) { + $this->alert(l('files.error.missing.file')); + $this->redirect($page); + } + + // setup the form and form action + $form = $file->form('edit', function($form) use($file, $page, $self) { + + $form->validate(); + + if(!$form->isValid()) { + return $self->alert(l('files.show.error.form')); + } + + try { + $file->update($form->serialize()); + $self->notify(':)'); + $self->redirect($file); + } catch(Exception $e) { + $self->alert($e->getMessage()); + } + + }); + + return $this->screen('files/edit', $file, array( + 'form' => $form, + 'page' => $page, + 'file' => $file, + 'returnTo' => url::last() == $page->url('files') ? $page->uri('files') : $page->uri('edit'), + 'uploader' => $this->snippet('uploader', array( + 'url' => $file->url('replace'), + 'accept' => $file->mime(), + 'multiple' => false + )) + )); + + } + + public function upload($id) { + + $page = $this->page($id); + + try { + $page->upload(); + $this->notify(':)'); + } catch(Exception $e) { + $this->alert($e->getMessage()); + } + + $this->redirect($page); + + } + + public function replace($id, $filename) { + + $page = $this->page($id); + $file = $this->file($page, $filename); + + try { + $file->replace(); + $this->notify(':)'); + } catch(Exception $e) { + $this->alert($e->getMessage()); + } + + $this->redirect($file); + + } + + public function context($id, $filename) { + + $page = $this->page($id); + $file = $this->file($page, $filename); + + return $file->menu(); + + } + + public function thumb($id, $filename) { + + $page = $this->page($id); + $file = $this->file($page, $filename); + $width = intval(get('width')); + $height = intval(get('height')); + + if(!$file->canHavePreview()) { + return response::error('No preview available', 404); + } + + if(!$file->canHaveThumb()) { + go($file->url()); + } + + if(get('crop') == true) { + $thumb = $file->crop($width, $height, 80); + } else { + $thumb = $file->resize($width, $height, 80); + } + + go($thumb->url()); + + } + + public function delete($id, $filename) { + + $self = $this; + $page = $this->page($id); + $file = $this->file($page, $filename); + $form = $this->form('files/delete', $file, function($form) use($file, $page, $self) { + + try { + $file->delete(); + $self->notify(':)'); + $self->redirect($page, 'edit'); + } catch(Exception $e) { + $form->alert($e->getMessage()); + } + + }); + + return $this->modal('files/delete', compact('form')); + + } + + protected function file($page, $filename) { + + $file = $page->file(File::decodeFilename($filename)); + + if(!$file) { + throw new Exception(l('files.error.missing.file')); + } + + return $file; + + } + + protected function sort($page) { + + if(!r::is('post') or get('action') != 'sort') return; + + $filenames = get('filenames'); + $counter = 0; + + foreach($filenames as $filename) { + if($file = $page->file($filename)) { + $counter++; + try { + $file->update('sort', $counter); + } catch(Exception $e) { + + } + } + } + + $this->redirect($page, 'files'); + + } + +} \ No newline at end of file diff --git a/panel/app/controllers/installation.php b/panel/app/controllers/installation.php new file mode 100644 index 0000000..e9fc0b7 --- /dev/null +++ b/panel/app/controllers/installation.php @@ -0,0 +1,76 @@ +isCompleted()) { + $this->redirect(); + } else if($problems = $installer->problems()) { + return $this->problems($problems); + } else { + return $this->signup(); + } + + } + + protected function problems($problems) { + $form = $this->form('installation/check', array($problems)); + return $this->modal('index', compact('form')); + } + + protected function signup() { + + $self = $this; + $form = $this->form('installation/signup', array(), function($form) use($self) { + + $form->validate(); + + if(!$form->isValid()) { + return false; + } + + try { + + // fetch all the form data + $data = $form->serialize(); + + // make sure that the first user is an admin + $data['role'] = 'admin'; + + // try to create the new user + $user = site()->users()->create($data); + + // store the new username for the login screen + s::set('username', $user->username()); + + // try to login the user automatically + if($user->hasPanelAccess()) { + $user->login($data['password']); + } + + // redirect to the login + $self->redirect('login'); + + } catch(Exception $e) { + $form->alert($e->getMessage()); + } + + }); + + return $this->modal('index', compact('form')); + + } + + public function modal($view, $data = array()) { + return $this->layout('base', array( + 'bodyclass' => 'installation', + 'content' => $this->view('installation/' . $view, $data) + )); + } + +} \ No newline at end of file diff --git a/panel/app/controllers/options.php b/panel/app/controllers/options.php new file mode 100644 index 0000000..d33c259 --- /dev/null +++ b/panel/app/controllers/options.php @@ -0,0 +1,40 @@ +site(); + $sidebar = $site->sidebar(); + $form = $site->form('edit', function($form) use($site, $self) { + + // validate all fields + $form->validate(); + + // stop at invalid fields + if(!$form->isValid()) { + return $self->alert(l('pages.show.error.form')); + } + + try { + $site->update($form->serialize()); + $self->notify(':)'); + return $self->redirect('options'); + } catch(Exception $e) { + return $self->alert($e->getMessage()); + } + + }); + + return $this->screen('options/index', $site, array( + 'site' => $site, + 'form' => $form, + 'files' => $sidebar->files(), + 'license' => panel()->license(), + 'uploader' => $this->snippet('uploader', array('url' => $site->url('upload'))) + )); + + } + +} \ No newline at end of file diff --git a/panel/app/controllers/pages.php b/panel/app/controllers/pages.php new file mode 100644 index 0000000..11d2d51 --- /dev/null +++ b/panel/app/controllers/pages.php @@ -0,0 +1,218 @@ +page($id); + $form = $parent->form('add', function($form) use($parent, $self) { + + $form->validate(); + + if(!$form->isValid()) { + return $form->alert(l('pages.add.error.template')); + } + + try { + + $data = $form->serialize(); + $page = $parent->children()->create($data['uid'], $data['template'], array( + 'title' => $data['title'] + )); + + $self->notify(':)'); + $this->redirect($page, 'edit'); + + } catch(Exception $e) { + $form->alert($e->getMessage()); + } + + }); + + return $this->modal('pages/add', compact('form')); + + } + + public function edit($id) { + + $self = $this; + + try { + $page = $this->page($id); + } catch(Exception $e) { + if($page = $this->page(dirname($id))) { + $this->alert(l('pages.error.missing')); + $this->redirect($page); + } + } + + $form = $page->form('edit', function($form) use($page, $self) { + + // validate all fields + $form->validate(); + + // stop at invalid fields + if(!$form->isValid()) { + return $self->alert(l('pages.show.error.form')); + } + + try { + $page->update($form->serialize()); + $self->notify(':)'); + return $self->redirect($page); + } catch(Exception $e) { + return $self->alert($e->getMessage()); + } + + }); + + return $this->screen('pages/edit', $page, array( + 'page' => $page, + 'sidebar' => $page->sidebar(), + 'form' => $form, + 'uploader' => $this->snippet('uploader', array('url' => $page->url('upload'))) + )); + + } + + public function delete($id) { + + $self = $this; + $page = $this->page($id); + + try { + $page->isDeletable(true); + } catch(Exception $e) { + return $this->modal('error', array( + 'headline' => l($e->getMessage() . '.headline'), + 'text' => l($e->getMessage() . '.text'), + 'back' => $page->url() + )); + } + + $form = $page->form('delete', function($form) use($page, $self) { + try { + $page->delete(); + $self->notify(':)'); + $self->redirect($page->parent()->isSite() ? '/' : $page->parent()); + } catch(Exception $e) { + $form->alert($e->getMessage()); + } + }); + + return $this->modal('pages/delete', compact('form')); + + } + + public function keep($id) { + $page = $this->page($id); + $page->changes()->keep(); + $this->redirect($page); + } + + public function discard($id) { + $page = $this->page($id); + $page->changes()->discard(); + $this->redirect($page); + } + + public function url($id) { + + $self = $this; + $page = $this->page($id); + + if(!$page->canChangeUrl()) { + return $this->modal('error', array( + 'headline' => l('error'), + 'text' => l('pages.url.error.rights'), + )); + } + + $form = $page->form('url', function($form) use($page, $self) { + + try { + $page->move(get('uid')); + $self->notify(':)'); + $self->redirect($page); + } catch(Exception $e) { + $form->alert($e->getMessage()); + $form->fields->uid->error = true; + } + + }); + + return $this->modal('pages/url', compact('form')); + + } + + public function template($id) { + + $self = $this; + $page = $this->page($id); + + if(!$page->canChangeTemplate()) { + return $this->modal('error', array( + 'headline' => l('error'), + 'text' => l('pages.template.error'), + )); + } + + if($info = get('info')) { + $prep = $page->prepareForNewTemplate($page->blueprint()->name(), $info); + return $this->snippet('template', $prep); + } + + $form = $page->form('template', function($form) use($page, $self) { + + try { + + $data = $form->serialize(); + + $page->changeTemplate(a::get($data, 'template')); + + $self->notify(':)'); + $self->redirect($page); + } catch(Exception $e) { + $form->alert($e->getMessage()); + } + + }); + + return $this->modal('pages/template', compact('form')); + + } + + public function toggle($id) { + + $self = $this; + $page = $this->page($id); + + if($page->isErrorPage()) { + return $this->modal('error', array( + 'headline' => l('error'), + 'text' => l('pages.toggle.error.error'), + )); + } + + $form = $page->form('toggle', function($form) use($page, $self) { + + try { + $page->toggle(get('position', 'last')); + $self->notify(':)'); + $self->redirect($page); + } catch(Exception $e) { + $form->alert($e->getMessage()); + } + + }); + + return $this->modal('pages/toggle', compact('form')); + + } + + public function context($id) { + return $this->page($id)->menu('context'); + } + +} \ No newline at end of file diff --git a/panel/app/controllers/search.php b/panel/app/controllers/search.php new file mode 100644 index 0000000..b2a43ff --- /dev/null +++ b/panel/app/controllers/search.php @@ -0,0 +1,18 @@ +view('search/results', array( + 'pages' => $search->pages(), + 'users' => $search->users(), + )); + + } + +} \ No newline at end of file diff --git a/panel/app/controllers/subpages.php b/panel/app/controllers/subpages.php new file mode 100644 index 0000000..7cac180 --- /dev/null +++ b/panel/app/controllers/subpages.php @@ -0,0 +1,91 @@ +page($id); + + // don't create the view if the page is not allowed to have subpages + if(!$page->canHaveSubpages()) { + throw new Exception(l('subpages.add.error')); + } + + // get the subpages + $visible = $this->visible($page); + $invisible = $this->invisible($page); + + // activate the sorting + $this->sort($page); + + return $this->screen('subpages/index', $page, array( + 'page' => $page, + 'addbutton' => $page->addbutton(), + 'sortable' => $page->blueprint()->pages()->sortable(), + 'flip' => $page->blueprint()->pages()->sort() == 'flip', + 'visible' => $visible, + 'invisible' => $invisible, + )); + + } + + protected function subpages($page, $type) { + + $pages = $page->children()->$type()->paginated('subpages/' . $type); + $pagination = $this->snippet('pagination', array( + 'pagination' => $pages->pagination(), + 'nextUrl' => $pages->pagination()->nextPageUrl(), + 'prevUrl' => $pages->pagination()->prevPageUrl(), + )); + + return new Obj(array( + 'pages' => $pages, + 'pagination' => $pagination, + 'start' => $pages->pagination()->numStart(), + 'total' => $pages->pagination()->items(), + 'firstPage' => $pages->pagination()->firstPageUrl(), + )); + + } + + protected function visible($page) { + return $this->subpages($page, 'visible'); + } + + protected function invisible($page) { + return $this->subpages($page, 'invisible'); + } + + protected function sort($page) { + + // handle sorting + if(r::is('post') and $action = get('action') and $id = get('id')) { + + $subpage = $this->page($page->id() . '/' . $id); + + switch($action) { + case 'sort': + try { + $subpage->sort(get('to')); + } catch(Exception $e) { + // no error handling, because if sorting + // breaks, the refresh will fix it. + } + break; + case 'hide': + try { + $subpage->hide(); + } catch(Exception $e) { + // no error handling, because if sorting + // breaks, the refresh will fix it. + } + break; + } + + $this->redirect($page, 'subpages'); + + } + + } + +} \ No newline at end of file diff --git a/panel/app/controllers/users.php b/panel/app/controllers/users.php new file mode 100644 index 0000000..ba41eea --- /dev/null +++ b/panel/app/controllers/users.php @@ -0,0 +1,134 @@ +users()->paginate(20, array('method' => 'query')); + $admin = panel()->user()->isAdmin(); + $pagination = $this->snippet('pagination', array( + 'pagination' => $users->pagination(), + 'nextUrl' => $users->pagination()->nextPageUrl(), + 'prevUrl' => $users->pagination()->prevPageUrl(), + )); + + return $this->screen('users/index', $users, array( + 'users' => $users, + 'admin' => $admin, + 'pagination' => $pagination + )); + + } + + public function add() { + + if(!panel()->user()->isAdmin()) { + $this->redirect('users'); + } + + $self = $this; + $form = $this->form('users/user', null, function($form) use($self) { + + $form->validate(); + + if(!$form->isValid()) { + return false; + } + + $data = $form->serialize(); + + try { + $user = panel()->users()->create($data); + $self->notify(':)'); + $self->redirect('users'); + } catch(Exception $e) { + $self->alert($e->getMessage()); + } + + }); + + return $this->screen('users/edit', 'user', array( + 'user' => null, + 'form' => $form, + 'writable' => is_writable(kirby()->roots()->accounts()), + 'uploader' => null + )); + + } + + public function edit($username) { + + $self = $this; + $user = $this->user($username); + + if(!panel()->user()->isAdmin() and !$user->isCurrent()) { + $this->redirect('users'); + } + + $form = $user->form('user', function($form) use($user, $self) { + + $form->validate(); + + if(!$form->isValid()) { + return false; + } + + $data = $form->serialize(); + + try { + $user->update($data); + $self->notify(':)'); + $self->redirect($user, 'edit'); + } catch(Exception $e) { + $self->alert($e->getMessage()); + } + + }); + + return $this->screen('users/edit', $user, array( + 'user' => $user, + 'form' => $form, + 'writable' => is_writable(kirby()->roots()->accounts()), + 'uploader' => $this->snippet('uploader', array( + 'url' => $user->url('avatar'), + 'accept' => 'image/jpeg,image/png,image/gif', + 'multiple' => false + )) + )); + + } + + public function delete($username) { + + $user = $this->user($username); + $self = $this; + + if(!panel()->user()->isAdmin() and !$user->isCurrent()) { + return $this->modal('error', array( + 'headline' => l('error'), + 'text' => l('users.delete.error.rights'), + 'back' => purl('users') + )); + } else { + + $form = $user->form('delete', function($form) use($user, $self) { + + try { + $user->delete(); + $self->notify(':)'); + $self->redirect('users'); + } catch(Exception $e) { + $form->alert($e->getMessage()); + } + + }); + + return $this->modal('users/delete', compact('form')); + + } + + } + +} \ No newline at end of file diff --git a/panel/app/fields/base/base.php b/panel/app/fields/base/base.php new file mode 100644 index 0000000..2d991ce --- /dev/null +++ b/panel/app/fields/base/base.php @@ -0,0 +1,191 @@ + array(), 'css' => array()); + + public $id; + public $name; + public $input; + public $label; + public $icon; + public $type; + public $help; + public $value; + public $text; + public $autofocus; + public $placeholder; + public $options; + public $content; + public $readonly; + public $disabled; + public $required; + public $validate; + public $width; + public $default; + public $error = false; + public $parentField = false; + public $page; + public $model; + + public function root() { + $obj = new ReflectionClass($this); + return dirname($obj->getFileName()); + } + + public function validate() { + + try { + + if(!$this->validate) { + return true; + } else if(is_array($this->validate)) { + foreach($this->validate as $validator => $options) { + if(!is_null($options)) { + if(is_numeric($validator)) { + $result = call('v::' . $options, $this->value()); + } else { + $result = call('v::' . $validator, array($this->value(), $options)); + } + if(!$result) return false; + } + } + return true; + } else { + return call('v::' . $this->validate, $this->value()); + } + + } catch(Exception $e) { + return true; + } + + } + + public function result() { + return get($this->name()); + } + + public function __call($name, $args) { + return isset($this->{$name}) ? $this->{$name} : null; + } + + public function id() { + if(!is_null($this->id)) return $this->id; + return 'form-field-' . $this->name; + } + + public function label() { + + if(!$this->label) return null; + + $label = new Brick('label', $this->i18n($this->label)); + $label->addClass('label'); + $label->attr('for', $this->id()); + + if($this->required()) { + $label->append(new Brick('abbr', '*', array('title' => l::get('required', 'Required')))); + } + + return $label; + + } + + public function i18n($value) { + return i18n($value); + } + + public function icon() { + + if(empty($this->icon)) { + return null; + } else if($this->readonly() and empty($this->icon)) { + $this->icon = 'lock'; + } + + $i = new Brick('i'); + $i->addClass('icon fa fa-' . $this->icon); + + $icon = new Brick('div'); + $icon->addClass('field-icon'); + $icon->append($i); + + return $icon; + + } + + public function help() { + + if(!$this->help) return null; + + $help = new Brick('div'); + $help->addClass('field-help marginalia text'); + $help->html($this->i18n($this->help)); + return $help; + + } + + public function input() { + return $this->input; + } + + public function content() { + + $content = new Brick('div'); + $content->addClass('field-content'); + $content->append($this->input()); + $content->append($this->icon()); + return $content; + + } + + public function element() { + + $element = new Brick('div'); + + $element->addClass('field'); + $element->addClass('field-grid-item'); + + if($this->error) { + $element->addClass('field-with-error'); + } + + if($this->width) { + $element->addClass('field-grid-item-' . str_replace('/', '-', $this->width)); + } + + if($this->readonly) { + $element->addClass('field-is-readonly'); + } + + if($this->disabled) { + $element->addClass('field-is-disabled'); + } + + if($this->icon) { + $element->addClass('field-with-icon'); + } + + $element->addClass('field-name-' . $this->name); + + return $element; + + } + + public function template() { + + return $this->element() + ->append($this->label()) + ->append($this->content()) + ->append($this->help()); + + } + + public function __toString() { + try { + return (string)$this->template(); + } catch(Exception $e) { + return (string)$e->getMessage(); + } + } + +} \ No newline at end of file diff --git a/panel/app/fields/checkbox/checkbox.php b/panel/app/fields/checkbox/checkbox.php new file mode 100644 index 0000000..c1db055 --- /dev/null +++ b/panel/app/fields/checkbox/checkbox.php @@ -0,0 +1,46 @@ +addClass('checkbox'); + $input->attr(array( + 'id' => $this->id(), + 'name' => $this->name(), + 'required' => $this->required(), + 'autofocus' => $this->autofocus(), + 'autocomplete' => $this->autocomplete(), + 'readonly' => $this->readonly(), + 'type' => 'checkbox', + 'checked' => v::accepted($this->value()), + )); + + $wrapper = parent::input(); + $wrapper->tag('label'); + $wrapper->text($this->i18n($this->text())); + $wrapper->attr('for', $this->id()); + $wrapper->removeAttr('id'); + $wrapper->addClass('input-with-checkbox'); + $wrapper->prepend($input); + + return $wrapper; + + } + + public function value() { + $value = parent::value(); + return empty($value) ? '0' : $value; + } + + public function result() { + $result = parent::result(); + return v::accepted($result) ? '1' : '0'; + } + + public function validate() { + return v::accepted($this->value()) or v::denied($this->value()); + } + +} \ No newline at end of file diff --git a/panel/app/fields/checkboxes/checkboxes.php b/panel/app/fields/checkboxes/checkboxes.php new file mode 100644 index 0000000..fb100ae --- /dev/null +++ b/panel/app/fields/checkboxes/checkboxes.php @@ -0,0 +1,45 @@ +replaceClass('radio', 'checkbox'); + $input->attr(array( + 'name' => $this->name() . '[]', + 'type' => 'checkbox', + 'value' => $value, + 'checked' => ($this->value === 'all') ? true : in_array($value, (array)$this->value()), + 'required' => false, + )); + + return $input; + + } + + public function value() { + + $value = InputListField::value(); + + if(is_array($value)) { + return $value; + } else { + return str::split($value, ','); + } + + } + + public function result() { + $result = parent::result(); + return is_array($result) ? implode(', ', $result) : ''; + } + + public function item($value, $text) { + $item = parent::item($value, $text); + $item->replaceClass('input-with-radio', 'input-with-checkbox'); + return $item; + } + +} diff --git a/panel/app/fields/date/assets/js/date.js b/panel/app/fields/date/assets/js/date.js new file mode 100755 index 0000000..8d97e6a --- /dev/null +++ b/panel/app/fields/date/assets/js/date.js @@ -0,0 +1,50 @@ +(function($) { + + $.fn.date = function() { + + return this.each(function() { + + if($(this).data('pikaday')) { + return $(this); + } + + var input = $(this).attr('type', 'text'); + var hidden = input.next(); + var format = input.data('format'); + var val = input.val(); + var date = val ? moment(val).format(format) : null; + + input.attr('placeholder', format); + input.val(date); + + // don't initialize the datepicker on readonly fields + if(input.is('[readonly]')) { + return false; + } + + input.on('change', function() { + var val = input.val(); + if(val) { + hidden.val(moment(val, format).format('YYYY-MM-DD')); + } else { + hidden.val(''); + } + }); + + var pikaday = new Pikaday({ + field : this, + firstDay : 1, + format : format, + i18n : input.data('i18n'), + onSelect : function(date) { + hidden.val(moment(date).format('YYYY-MM-DD')); + } + }); + + $(this).data('pikaday', pikaday); + + }); + + }; + +})(jQuery); \ No newline at end of file diff --git a/panel/app/fields/date/date.php b/panel/app/fields/date/date.php new file mode 100644 index 0000000..cd6648c --- /dev/null +++ b/panel/app/fields/date/date.php @@ -0,0 +1,63 @@ + array( + 'date.js' + ) + ); + + public function __construct() { + + $this->type = 'date'; + $this->icon = 'calendar'; + $this->label = l::get('fields.date.label', 'Date'); + $this->format = 'YYYY-MM-DD'; + + } + + public function format() { + $format = str::upper($this->format); + return empty($format) ? 'YYYY-MM-DD' : $format; + } + + public function validate() { + return v::date($this->result()); + } + + public function value() { + if($this->override()) { + $this->value = $this->default(); + } + return !empty($this->value) ? date('Y-m-d', strtotime($this->value)) : null; + } + + public function input() { + + $input = parent::input(); + $input->removeAttr('name'); + $input->data(array( + 'field' => 'date', + 'format' => $this->format(), + 'i18n' => html(json_encode(array( + 'previousMonth' => '‹', + 'nextMonth' => '›', + 'months' => l::get('fields.date.months'), + 'weekdays' => l::get('fields.date.weekdays'), + 'weekdaysShort' => l::get('fields.date.weekdays.short') + )), false) + )); + + $hidden = new Brick('input', null); + $hidden->type = 'hidden'; + $hidden->name = $this->name(); + $hidden->value = $this->value(); + + return $input . $hidden; + + } + +} diff --git a/panel/app/fields/datetime/datetime.php b/panel/app/fields/datetime/datetime.php new file mode 100644 index 0000000..476eb7c --- /dev/null +++ b/panel/app/fields/datetime/datetime.php @@ -0,0 +1,86 @@ +date = array( + 'format' => 'YYYY-MM-DD' + ); + + $this->time = array( + 'interval' => 60, + 'format' => 24 + ); + + } + + public function validate() { + + $result = $this->result(); + + if(empty($result)) { + return !$this->required(); + } else { + return v::date($result); + } + + } + + public function result() { + + $value = array_filter($this->value()); + + if(empty($value) or !isset($value['date']) or !isset($value['time'])) { + return ''; + } + + return a::get($value, 'date') . ' ' . a::get($value, 'time') . ':00'; + + } + + public function content() { + + if(is_array($this->value())) { + $timestamp = strtotime($this->result()); + } else { + $timestamp = strtotime($this->value()); + } + + $dateDefault = a::get($this->date, 'default', ($this->required() ? 'now' : false)); + $timeDefault = a::get($this->time, 'default', ($this->required() ? 'now' : false)); + + $dateValue = $timestamp ? date('Y-m-d', $timestamp) : $dateDefault; + $timeValue = $timestamp ? date('H:i', $timestamp) : $timeDefault; + + $date = form::field('date', array( + 'name' => $this->name() . '[date]', + 'value' => $dateValue, + 'format' => a::get($this->date, 'format', 'YYYY-MM-DD'), + 'id' => 'form-field-' . $this->name() . '-date', + 'required' => $this->required(), + 'readonly' => $this->readonly(), + )); + + $time = form::field('time', array( + 'name' => $this->name() . '[time]', + 'value' => $timeValue, + 'format' => a::get($this->time, 'format', 24), + 'interval' => a::get($this->time, 'interval', 60), + 'id' => 'form-field-' . $this->name() . '-time', + 'required' => $this->required(), + 'readonly' => $this->readonly(), + )); + + $grid = '
'; + $grid .= '
' . $date->content() . '
'; + $grid .= '
' . $time->content() . '
'; + $grid .= '
'; + + return $grid; + + } + +} \ No newline at end of file diff --git a/panel/app/fields/email/email.php b/panel/app/fields/email/email.php new file mode 100644 index 0000000..aa112ed --- /dev/null +++ b/panel/app/fields/email/email.php @@ -0,0 +1,35 @@ +type = 'email'; + $this->icon = 'envelope'; + $this->label = l::get('fields.email.label', 'Email'); + $this->placeholder = l::get('fields.email.placeholder', 'mail@example.com'); + $this->autocomplete = true; + + } + + public function input() { + + $input = parent::input(); + + if($this->autocomplete) { + $input->attr('autocomplete', 'off'); + $input->data(array( + 'field' => 'autocomplete', + 'url' => panel()->urls()->api() . '/autocomplete/emails?_csrf=' . panel()->csrf() + )); + } + + return $input; + + } + + public function validate() { + return v::email($this->result()); + } + +} \ No newline at end of file diff --git a/panel/app/fields/filename/filename.php b/panel/app/fields/filename/filename.php new file mode 100644 index 0000000..92974b3 --- /dev/null +++ b/panel/app/fields/filename/filename.php @@ -0,0 +1,18 @@ +addClass('field-icon'); + $icon->append('.' . $this->extension . ''); + + return $icon; + + } + +} diff --git a/panel/app/fields/headline/assets/css/headline.css b/panel/app/fields/headline/assets/css/headline.css new file mode 100644 index 0000000..00c0160 --- /dev/null +++ b/panel/app/fields/headline/assets/css/headline.css @@ -0,0 +1,19 @@ +.field-with-headline { + counter-increment: count; +} +.field-with-headline:first-child { + padding-top: 0; +} +.field-with-headline { + padding-top: 6em; +} +.field-with-headline .hgroup span { + padding-left: 1.5em; +} +.field-with-headline .hgroup:before { + position: absolute; + content: counter(count, decimal-leading-zero); + left: 0; + color: #8dae28; + font-weight: 400; +} diff --git a/panel/app/fields/headline/headline.php b/panel/app/fields/headline/headline.php new file mode 100644 index 0000000..4b9126b --- /dev/null +++ b/panel/app/fields/headline/headline.php @@ -0,0 +1,29 @@ + array( + 'headline.css' + ) + ); + + public function result() { + return null; + } + + public function label() { + return null; + } + + public function content() { + return '

' . html($this->i18n($this->label)) . '

'; + } + + public function element() { + $element = parent::element(); + $element->addClass('field-with-headline'); + return $element; + } + +} diff --git a/panel/app/fields/hidden/hidden.php b/panel/app/fields/hidden/hidden.php new file mode 100644 index 0000000..33d61d7 --- /dev/null +++ b/panel/app/fields/hidden/hidden.php @@ -0,0 +1,13 @@ + 'hidden', + 'name' => $this->name(), + 'value' => $this->value() + )); + } + +} \ No newline at end of file diff --git a/panel/app/fields/image/assets/css/image.css b/panel/app/fields/image/assets/css/image.css new file mode 100644 index 0000000..9f01aba --- /dev/null +++ b/panel/app/fields/image/assets/css/image.css @@ -0,0 +1,19 @@ +.field-with-image select { + margin-left: 3rem; +} +.field-with-image .input-preview { + position: absolute; + top: 2px; + left: 2px; + bottom: 2px; + width: 2.75em; + background: url(../images/pattern.png); +} +.field-with-image .input-preview figure { + display: block; + width: 100%; + height: 100%; + background-repeat: no-repeat; + background-position: center center; + background-size: cover; +} \ No newline at end of file diff --git a/panel/app/fields/image/assets/js/image.js b/panel/app/fields/image/assets/js/image.js new file mode 100644 index 0000000..8bf882b --- /dev/null +++ b/panel/app/fields/image/assets/js/image.js @@ -0,0 +1,55 @@ +(function($) { + + $.fn.imagefield = function() { + + return this.each(function() { + + var field = $(this); + + // avoid multiple init + if(field.data('imagefield')) return true; + field.data('imagefield', true); + + var select = field.find('select'); + var preview = field.find('.input-preview figure'); + var link = preview.parent('a'); + + select.on('keydown change', function() { + + var option = select.find('option:selected'); + var url = option.data('url'); + var thumb = option.data('thumb'); + + if(option.val() === '') { + url = '#'; + } + + if(thumb) { + preview.attr('style', 'background-image: url(' + thumb + ')'); + } else { + preview.attr('style', 'background-image: none'); + } + + link.attr('href', url); + + }).trigger('change'); + + field.find('.input-preview').on('click', function() { + if($(this).attr('href') == '#') { + return false; + } + }); + + field.find('.input').droppable({ + hoverClass: 'over', + accept: $('.sidebar .draggable-file'), + drop: function(e, ui) { + $(this).find('select').val(ui.draggable.data('helper')).trigger('change'); + } + }); + + }); + + }; + +})(jQuery); \ No newline at end of file diff --git a/panel/app/fields/image/image.php b/panel/app/fields/image/image.php new file mode 100644 index 0000000..400aeb7 --- /dev/null +++ b/panel/app/fields/image/image.php @@ -0,0 +1,94 @@ +icon = 'image'; + } + + public function element() { + $element = parent::element(); + $element->addClass('field-with-image'); + $element->data('field', 'imagefield'); + return $element; + } + + public function image() { + return $this->page->image($this->value()); + } + + public function preview() { + + $figure = new Brick('figure'); + + if($image = $this->image()) { + $figure->attr('style', 'background-image: url(' . $image->crop(75)->url() . ')'); + $url = $image->url('edit'); + } else { + $figure->attr('style', 'background-image: url(' . $this->value() . ')'); + $url = ''; + } + + return '
' . $figure . ''; + + } + + public function input() { + return $this->preview() . parent::input(); + } + + public function option($filename, $image, $selected = false) { + + if($image == '') { + return new Brick('option', '', array( + 'value' => '', + 'selected' => $selected + )); + } else { + return new Brick('option', $image->filename(), array( + 'value' => $filename, + 'selected' => $selected, + 'data-url' => $image->url('edit'), + 'data-thumb' => $image->crop(75)->url() + )); + } + + } + + public function options() { + + $options = []; + + foreach($this->images() as $image) { + $options[$image->filename()] = $image; + } + + return $options; + + } + + public function images() { + + $images = $this->page->images(); + + if(!empty($this->extension)) { + + if(!is_array($this->extension)) { + $extensions = [$this->extension]; + } else { + $extensions = $this->extension; + } + + $images = $images->filter(function($image) use($extensions) { + return in_array(strtolower($image->extension()), $extensions); + }); + + } + + return $images; + + } + +} diff --git a/panel/app/fields/info/info.php b/panel/app/fields/info/info.php new file mode 100644 index 0000000..3f13ff3 --- /dev/null +++ b/panel/app/fields/info/info.php @@ -0,0 +1,21 @@ +addClass('field-with-icon'); + return $element; + } + + public function input() { + return '
' . kirbytext($this->i18n($this->text())) . '
'; + } + +} \ No newline at end of file diff --git a/panel/app/fields/input/input.php b/panel/app/fields/input/input.php new file mode 100644 index 0000000..73a8ce2 --- /dev/null +++ b/panel/app/fields/input/input.php @@ -0,0 +1,37 @@ +addClass('input'); + $input->attr(array( + 'type' => $this->type(), + 'value' => '', + 'required' => $this->required(), + 'name' => $this->name(), + 'autocomplete' => $this->autocomplete() === false ? 'off' : 'on', + 'autofocus' => $this->autofocus(), + 'placeholder' => $this->i18n($this->placeholder()), + 'readonly' => $this->readonly(), + 'disabled' => $this->disabled(), + 'id' => $this->id() + )); + + if(!is_array($this->value())) { + $input->val(html($this->value(), false)); + } + + if($this->readonly()) { + $input->attr('tabindex', '-1'); + $input->addClass('input-is-readonly'); + } + + return $input; + + } + +} \ No newline at end of file diff --git a/panel/app/fields/inputlist/inputlist.php b/panel/app/fields/inputlist/inputlist.php new file mode 100644 index 0000000..9ca8dc9 --- /dev/null +++ b/panel/app/fields/inputlist/inputlist.php @@ -0,0 +1,83 @@ +removeClass('input'); + return $input; + } + + public function options() { + return fieldoptions::build($this); + } + + public function item($value, $text) { + + $input = $this->input($value); + + $label = new Brick('label', $this->i18n($text)); + $label->addClass('input'); + $label->attr('data-focus', 'true'); + $label->prepend($input); + + if($this->readonly) { + $label->addClass('input-is-readonly'); + } + + return $label; + + } + + public function content() { + + $html = '
    '; + + switch($this->columns()) { + case 2: + $width = ' field-grid-item-1-2'; + break; + case 3: + $width = ' field-grid-item-1-3'; + break; + case 4: + $width = ' field-grid-item-1-4'; + break; + case 5: + $width = ' field-grid-item-1-5'; + break; + default: + $width = ''; + break; + } + + foreach($this->options() as $key => $value) { + $html .= '
  • '; + $html .= $this->item($key, $value); + $html .= '
  • '; + } + + $html .= '
'; + + $content = new Brick('div'); + $content->addClass('field-content'); + $content->append($html); + + return $content; + + } + + public function validate() { + if(is_array($this->value())) { + foreach($this->value() as $v) { + if(!array_key_exists($v, $this->options())) return false; + } + return true; + } else { + return array_key_exists($this->value(), $this->options()); + } + } + +} diff --git a/panel/app/fields/line/line.php b/panel/app/fields/line/line.php new file mode 100644 index 0000000..b789b47 --- /dev/null +++ b/panel/app/fields/line/line.php @@ -0,0 +1,19 @@ +'; + } + + public function element() { + $element = parent::element(); + $element->addClass('field-with-line'); + return $element; + } + +} \ No newline at end of file diff --git a/panel/app/fields/number/number.php b/panel/app/fields/number/number.php new file mode 100644 index 0000000..2e8c838 --- /dev/null +++ b/panel/app/fields/number/number.php @@ -0,0 +1,39 @@ +type = 'number'; + $this->label = l::get('fields.number.label', 'Number'); + $this->placeholder = l::get('fields.number.placeholder', '#'); + $this->step = 1; + $this->min = false; + $this->max = false; + + } + + public function input() { + $input = parent::input(); + $input->attr('step', $this->step); + $input->attr('min', $this->min); + $input->attr('max', $this->max); + return $input; + } + + public function validate() { + + if(!v::num($this->result())) return false; + + if($this->validate and is_array($this->validate)) { + return parent::validate(); + } else { + if(is_numeric($this->min) and !v::min($this->result(), $this->min)) return false; + if(is_numeric($this->max) and !v::max($this->result(), $this->max)) return false; + } + + return true; + + } + +} diff --git a/panel/app/fields/page/page.php b/panel/app/fields/page/page.php new file mode 100644 index 0000000..00435bf --- /dev/null +++ b/panel/app/fields/page/page.php @@ -0,0 +1,25 @@ +icon = 'chain'; + $this->label = l::get('fields.page.label', 'Page'); + $this->placeholder = l::get('fields.page.placeholder', 'path/to/page'); + + } + + public function input() { + + $input = parent::input(); + $input->data(array( + 'field' => 'autocomplete', + 'url' => panel()->urls()->api() . '/autocomplete/uris' + )); + + return $input; + + } + +} \ No newline at end of file diff --git a/panel/app/fields/password/password.php b/panel/app/fields/password/password.php new file mode 100644 index 0000000..9ed257a --- /dev/null +++ b/panel/app/fields/password/password.php @@ -0,0 +1,40 @@ +type = 'password'; + $this->icon = 'key'; + $this->label = l('fields.password.label', 'Password'); + + } + + public function input() { + + $input = parent::input(); + + if($this->suggestion) { + $input->data(array( + 'field' => 'passwordSuggestion' + )); + } + + return $input; + + } + + public function help() { + if($this->suggestion and !$this->readonly) { + $this->help = $this->suggestion(); + } + return parent::help(); + } + + public function suggestion() { + return ''; + } + +} \ No newline at end of file diff --git a/panel/app/fields/radio/radio.php b/panel/app/fields/radio/radio.php new file mode 100644 index 0000000..39c4d0c --- /dev/null +++ b/panel/app/fields/radio/radio.php @@ -0,0 +1,41 @@ +options(); + if(is_array($options)) { + reset($options); + $value = key($options); + } + } + return $value; + } + + public function input() { + + $val = func_get_arg(0); + $input = parent::input(); + $input->addClass('radio'); + $input->attr('type', 'radio'); + $input->val($val); + + if($this->readonly) { + $input->attr('disabled', true); + } + + $input->attr('checked', $val == $this->value()); + return $input; + + } + + public function item($value, $text) { + $item = parent::item($value, $text); + $item->addClass('input-with-radio'); + return $item; + } + +} diff --git a/panel/app/fields/select/select.php b/panel/app/fields/select/select.php new file mode 100644 index 0000000..b1403b4 --- /dev/null +++ b/panel/app/fields/select/select.php @@ -0,0 +1,74 @@ +type = 'select'; + $this->options = array(); + $this->icon = 'chevron-down'; + + } + + public function options() { + return FieldOptions::build($this); + } + + public function option($value, $text, $selected = false) { + return new Brick('option', $this->i18n($text), array( + 'value' => $value, + 'selected' => $selected + )); + } + + public function input() { + + $select = new Brick('select'); + $select->addClass('selectbox'); + $select->attr(array( + 'name' => $this->name(), + 'id' => $this->id(), + 'required' => $this->required(), + 'autocomplete' => $this->autocomplete(), + 'autofocus' => $this->autofocus(), + 'readonly' => $this->readonly(), + 'disabled' => $this->disabled(), + )); + + $default = $this->default(); + + if(!$this->required()) { + $select->append($this->option('', '', $this->value() == '')); + } + + if($this->readonly()) { + $select->attr('tabindex', '-1'); + } + + foreach($this->options() as $value => $text) { + $select->append($this->option($value, $text, $this->value() == $value)); + } + + $inner = new Brick('div'); + $inner->addClass('selectbox-wrapper'); + $inner->append($select); + + $wrapper = new Brick('div'); + $wrapper->addClass('input input-with-selectbox'); + $wrapper->append($inner); + + if($this->readonly()) { + $wrapper->addClass('input-is-readonly'); + } else { + $wrapper->attr('data-focus', 'true'); + } + + return $wrapper; + + } + + public function validate() { + return array_key_exists($this->value(), $this->options()); + } + +} diff --git a/panel/app/fields/structure/assets/css/structure.css b/panel/app/fields/structure/assets/css/structure.css new file mode 100644 index 0000000..ab59695 --- /dev/null +++ b/panel/app/fields/structure/assets/css/structure.css @@ -0,0 +1,97 @@ +.structure { + padding-bottom: .5em; +} +.structure-entry { + background: #fff; + border: 2px solid #ddd; + margin-bottom: .5em; +} +.structure-readonly .structure-entry { + background: #efefef; + color: #777; +} +.structure-entry:last-child { + margin-bottom: 0; +} +.structure-entry-content { + padding: 1em 1.5em; + border-bottom: 1px solid #efefef; +} +.structure[data-sortable=true] .structure-entry-content { + cursor: move; +} +.structure-entry-options .btn { + padding: .75em 1.5em; + width: 50%; + float: left; + border-right: 1px solid #efefef; +} +.structure-empty { + padding: 1.5em; + background: #ddd; +} +.fileview-sidebar .structure-empty { + background: none; + border-radius: 5px; + border: 1px dashed #ddd; + padding: 1rem 1.5rem 1.25rem; +} +.structure-empty a { + border-bottom: 2px solid #aaa; + margin-left: .5em; +} +.fileview-sidebar .structure-empty a { + display: inline-block; + margin-left: 0; +} +.structure-empty a:hover { + border-color: #000; +} +.structure-add-button { + cursor: pointer; +} + + +/* Table */ +.structure-table { + width: 100%; + border-spacing: 0; + border: 2px solid #ddd; + border-bottom: 1px solid #ddd; + border-right: 1px solid #ddd; + table-layout: fixed; +} +.structure-table td, .structure-table th { + background: #fff; + border-bottom: 1px solid #ddd; + border-right: 1px solid #ddd; + text-align: left; + vertical-align: top; +} +.structure-table th { + padding: .5em; + font-weight: 400; + color: #777; + font-style: italic; +} +.structure-table td a { + display: block; + padding: .5em; + overflow: hidden; + width: 100%; + text-overflow: ellipsis; + cursor: move; +} +.structure-table-options { + width: 3rem; + text-align: center; +} +.structure-table .structure-table-options a { + text-align: center; + cursor: pointer; +} + +.structure-sortable-helper { + border-top: 1px solid #ddd; + border-left: 1px solid #ddd; +} diff --git a/panel/app/fields/structure/assets/js/structure.js b/panel/app/fields/structure/assets/js/structure.js new file mode 100644 index 0000000..dd6559e --- /dev/null +++ b/panel/app/fields/structure/assets/js/structure.js @@ -0,0 +1,53 @@ +(function($) { + + var Structure = function(el) { + + var element = $(el); + var style = element.data('style'); + var api = element.data('api'); + var sortable = element.data('sortable'); + var entries = style == 'table' ? element.find('.structure-table tbody') : element.find('.structure-entries'); + + if(sortable === false) return false; + + entries.sortable({ + helper: function(e, ui) { + ui.children().each(function() { + $(this).width($(this).width()); + }); + return ui.addClass('structure-sortable-helper'); + }, + update: function() { + + var ids = []; + + $.each($(this).sortable('toArray'), function(i, id) { + ids.push(id.replace('structure-entry-', '')); + }); + + $.post(api, {ids: ids}, function() { + app.content.reload(); + }); + + } + }); + + }; + + $.fn.structure = function() { + + return this.each(function() { + + if($(this).data('structure')) { + return $(this); + } else { + var structure = new Structure(this); + $(this).data('structure', structure); + return $(this); + } + + }); + + }; + +})(jQuery); \ No newline at end of file diff --git a/panel/app/fields/structure/controller.php b/panel/app/fields/structure/controller.php new file mode 100644 index 0000000..c38ab73 --- /dev/null +++ b/panel/app/fields/structure/controller.php @@ -0,0 +1,93 @@ +model(); + $structure = $this->structure($model); + $modalsize = $this->field()->modalsize(); + $form = $this->form('add', array($model, $structure), function($form) use($model, $structure, $self) { + + $form->validate(); + + if(!$form->isValid()) { + return false; + } + + $structure->add($form->serialize()); + $self->redirect($model); + + }); + + return $this->modal('add', compact('form', 'modalsize')); + + } + + public function update($entryId) { + + $self = $this; + $model = $this->model(); + $structure = $this->structure($model); + $entry = $structure->find($entryId); + + if(!$entry) { + return $this->modal('error', array( + 'text' => l('fields.structure.entry.error') + )); + } + + $modalsize = $this->field()->modalsize(); + $form = $this->form('update', array($model, $structure, $entry), function($form) use($model, $structure, $self, $entryId) { + + // run the form validator + $form->validate(); + + if(!$form->isValid()) { + return false; + } + + $structure->update($entryId, $form->serialize()); + $self->redirect($model); + + }); + + return $this->modal('update', compact('form', 'modalsize')); + + } + + public function delete($entryId) { + + $self = $this; + $model = $this->model(); + $structure = $this->structure($model); + $entry = $structure->find($entryId); + + if(!$entry) { + return $this->modal('error', array( + 'text' => l('fields.structure.entry.error') + )); + } + + $form = $this->form('delete', $model, function() use($self, $model, $structure, $entryId) { + $structure->delete($entryId); + $self->redirect($model); + }); + + return $this->modal('delete', compact('form')); + + } + + public function sort() { + $model = $this->model(); + $structure = $this->structure($model); + $structure->sort(get('ids')); + $this->redirect($model); + } + + protected function structure($model) { + return $model->structure()->forField($this->fieldname()); + } + +} \ No newline at end of file diff --git a/panel/app/fields/structure/forms/add.php b/panel/app/fields/structure/forms/add.php new file mode 100644 index 0000000..0e1b7b4 --- /dev/null +++ b/panel/app/fields/structure/forms/add.php @@ -0,0 +1,11 @@ +fields(), array(), $structure->field()); + $form->cancel($model); + $form->buttons->submit->value = l('add'); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/fields/structure/forms/delete.php b/panel/app/fields/structure/forms/delete.php new file mode 100644 index 0000000..6edc57e --- /dev/null +++ b/panel/app/fields/structure/forms/delete.php @@ -0,0 +1,17 @@ + array( + 'label' => 'fields.structure.delete.label', + 'type' => 'info', + ) + )); + + $form->style('delete'); + $form->cancel($model); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/fields/structure/forms/update.php b/panel/app/fields/structure/forms/update.php new file mode 100644 index 0000000..cef432a --- /dev/null +++ b/panel/app/fields/structure/forms/update.php @@ -0,0 +1,12 @@ +fields(), $entry->toArray(), $structure->field()); + + $form->cancel($model); + $form->buttons->submit->value = l('ok'); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/fields/structure/structure.php b/panel/app/fields/structure/structure.php new file mode 100644 index 0000000..659d99e --- /dev/null +++ b/panel/app/fields/structure/structure.php @@ -0,0 +1,162 @@ + array( + 'structure.js' + ), + 'css' => array( + 'structure.css' + ) + ); + + public $fields = array(); + public $entry = null; + public $structure = null; + public $style = 'items'; + public $modalsize = 'medium'; + + public function routes() { + + return array( + array( + 'pattern' => 'add', + 'method' => 'get|post', + 'action' => 'add' + ), + array( + 'pattern' => 'sort', + 'method' => 'post', + 'action' => 'sort', + ), + array( + 'pattern' => '(:any)/update', + 'method' => 'get|post', + 'action' => 'update' + ), + array( + 'pattern' => '(:any)/delete', + 'method' => 'get|post', + 'action' => 'delete', + ) + ); + } + + public function modalsize() { + $sizes = array('small', 'medium', 'large'); + return in_array($this->modalsize, $sizes) ? $this->modalsize : 'medium'; + } + + public function style() { + $styles = array('table', 'items'); + return in_array($this->style, $styles) ? $this->style : 'items'; + } + + public function structure() { + if(!is_null($this->structure)) { + return $this->structure; + } else { + return $this->structure = $this->model->structure()->forField($this->name); + } + } + + public function fields() { + + $output = array(); + + foreach($this->structure->fields() as $k => $v) { + $v['name'] = $k; + $v['value'] = '{{' . $k . '}}'; + $output[] = $v; + } + + return $output; + + } + + public function entries() { + return $this->structure()->data(); + } + + public function result() { + /** + * Users store their data as plain yaml. + * So we need this hacky solution to give data + * as an array to the form serializer in case + * of users, in order to not mess up their data + */ + if(is_a($this->model, 'Kirby\\Panel\\Models\\User')) { + return $this->structure()->toArray(); + } else { + return $this->structure()->toYaml(); + } + } + + public function entry($data) { + + if(is_null($this->entry) or !is_string($this->entry)) { + $html = array(); + foreach($this->fields as $name => $field) { + if(isset($data->$name)) { + $html[] = $data->$name; + } + } + return implode('
', $html); + } else { + + $text = $this->entry; + + foreach((array)$data as $key => $value) { + if(is_array($value)) { + $value = implode(', ', array_values($value)); + } + $text = str_replace('{{' . $key . '}}', $value, $text); + } + + return $text; + + } + + } + + public function label() { + return null; + } + + public function headline() { + + if(!$this->readonly) { + + $add = new Brick('a'); + $add->html('' . l('fields.structure.add')); + $add->addClass('structure-add-button label-option'); + $add->data('modal', true); + $add->attr('href', purl($this->model, 'field/' . $this->name . '/structure/add')); + + } else { + $add = null; + } + + // make sure there's at least an empty label + if(!$this->label) { + $this->label = ' '; + } + + $label = parent::label(); + $label->addClass('structure-label'); + $label->append($add); + + return $label; + + } + + public function content() { + return tpl::load(__DIR__ . DS . 'template.php', array('field' => $this)); + } + + public function url($action) { + return purl($this->model(), 'field/' . $this->name() . '/structure/' . $action); + } + +} \ No newline at end of file diff --git a/panel/app/fields/structure/styles/items.php b/panel/app/fields/structure/styles/items.php new file mode 100644 index 0000000..16bb511 --- /dev/null +++ b/panel/app/fields/structure/styles/items.php @@ -0,0 +1,18 @@ +entries() as $entry): ?> +
+
+ entry($entry) ?> +
+ readonly()): ?> + + +
+ \ No newline at end of file diff --git a/panel/app/fields/structure/styles/table.php b/panel/app/fields/structure/styles/table.php new file mode 100644 index 0000000..1d0aaa9 --- /dev/null +++ b/panel/app/fields/structure/styles/table.php @@ -0,0 +1,36 @@ + + + + fields() as $f): ?> + + + + + + + entries() as $entry): ?> + + fields() as $f): ?> + + + + + + +
+ i18n($f['label']), false) ?> + +   +
+ + {$f['name']})): ?> + {$f['name']}, false) ?> + +   + + + + + + +
\ No newline at end of file diff --git a/panel/app/fields/structure/template.php b/panel/app/fields/structure/template.php new file mode 100644 index 0000000..233c947 --- /dev/null +++ b/panel/app/fields/structure/template.php @@ -0,0 +1,20 @@ +
+ + headline() ?> + +
+ + entries()->count()): ?> +
+ +
+ + style() . '.php') ?> + +
+ +
\ No newline at end of file diff --git a/panel/app/fields/structure/views/add.php b/panel/app/fields/structure/views/add.php new file mode 100644 index 0000000..28805da --- /dev/null +++ b/panel/app/fields/structure/views/add.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/panel/app/fields/structure/views/delete.php b/panel/app/fields/structure/views/delete.php new file mode 100644 index 0000000..fd34175 --- /dev/null +++ b/panel/app/fields/structure/views/delete.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/panel/app/fields/structure/views/update.php b/panel/app/fields/structure/views/update.php new file mode 100644 index 0000000..28805da --- /dev/null +++ b/panel/app/fields/structure/views/update.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/panel/app/fields/tags/tags.php b/panel/app/fields/tags/tags.php new file mode 100644 index 0000000..d59cda9 --- /dev/null +++ b/panel/app/fields/tags/tags.php @@ -0,0 +1,52 @@ +icon = 'tag'; + $this->label = l::get('fields.tags.label', 'Tags'); + $this->index = 'siblings'; + $this->separator = ','; + $this->lower = false; + + } + + public function input() { + + $input = parent::input(); + $input->addClass('input-with-tags'); + $input->data(array( + 'field' => 'tags', + 'lowercase' => $this->lower ? 'true' : false, + 'separator' => $this->separator, + )); + + if(isset($this->data)) { + + $input->data('url', html(json_encode($this->data), false)); + + } else if($page = $this->page()) { + + $field = empty($this->field) ? $this->name() : $this->field; + $model = is_a($this->model, 'File') ? 'file' : 'page'; + + $query = array( + 'uri' => $page->id(), + 'index' => $this->index(), + 'field' => $field, + 'yaml' => $this->parentField, + 'model' => $model, + 'separator' => $this->separator(), + '_csrf' => panel()->csrf(), + ); + + $input->data('url', panel()->urls()->api() . '/autocomplete/field?' . http_build_query($query)); + + } + + return $input; + + } + +} diff --git a/panel/app/fields/tel/tel.php b/panel/app/fields/tel/tel.php new file mode 100644 index 0000000..61814a2 --- /dev/null +++ b/panel/app/fields/tel/tel.php @@ -0,0 +1,11 @@ +type = 'tel'; + $this->icon = 'phone'; + $this->label = l::get('fields.tel.label', 'Phone'); + } + +} \ No newline at end of file diff --git a/panel/app/fields/text/assets/css/counter.css b/panel/app/fields/text/assets/css/counter.css new file mode 100644 index 0000000..c4b0990 --- /dev/null +++ b/panel/app/fields/text/assets/css/counter.css @@ -0,0 +1,12 @@ +.field-counter { + position: absolute; + z-index: -1; + right: 0; + top: 0; + text-align: right; + font-size: .9em; + line-height: 1.66666666666667; +} +.field-counter.outside-range { + color: #b3000a; +} \ No newline at end of file diff --git a/panel/app/fields/text/assets/js/counter.js b/panel/app/fields/text/assets/js/counter.js new file mode 100644 index 0000000..2ef5458 --- /dev/null +++ b/panel/app/fields/text/assets/js/counter.js @@ -0,0 +1,34 @@ +(function($) { + + $.fn.counter = function() { + + return this.each(function() { + + var counter = $(this); + + if(counter.data('counter')) { + return counter; + } + + var field = counter.parent('.field').find('.input'); + var length = $.trim(field.val()).length; + var max = field.data('max'); + var min = field.data('min'); + + field.keyup(function() { + length = $.trim(field.val()).length; + counter.text(length + (max ? '/' + max : '')); + if((max && length > max) || (min && length < min)) { + counter.addClass('outside-range'); + } else { + counter.removeClass('outside-range'); + } + }).trigger('keyup'); + + counter.data('counter', true); + + }); + + }; + +}(jQuery)); \ No newline at end of file diff --git a/panel/app/fields/text/text.php b/panel/app/fields/text/text.php new file mode 100644 index 0000000..8150a7f --- /dev/null +++ b/panel/app/fields/text/text.php @@ -0,0 +1,76 @@ + 0, + 'max' => null + ); + + static public $assets = array( + 'js' => array( + 'counter.js' + ) + ); + + public function min() { + return isset($this->validate['min']) ? $this->validate['min'] : false; + } + + public function max() { + return isset($this->validate['max']) ? $this->validate['max'] : false; + } + + public function input() { + + $input = parent::input(); + + if(!$this->readonly() && ($this->min() || $this->max())) { + $input->data('max', $this->max())->data('min', $this->min()); + } + + return $input; + + } + + public function outsideRange($length) { + + if($this->min() && $length < $this->min()) return true; + if($this->max() && $length > $this->max()) return true; + + return false; + + } + + public function counter() { + + if(!$this->min() && !$this->max() || $this->readonly()) return null; + + $counter = new Brick('div'); + $counter->addClass('field-counter marginalia text'); + + $length = str::length($this->value()); + + if($this->outsideRange($length)) { + $counter->addClass('outside-range'); + } + + $counter->data('field', 'counter'); + $counter->html($length . ($this->max() ? '/' . $this->max() : '')); + + return $counter; + + } + + public function template() { + + return $this->element() + ->append($this->label()) + ->append($this->content()) + ->append($this->counter()) + ->append($this->help()); + + } + +} diff --git a/panel/app/fields/textarea/assets/js/editor.js b/panel/app/fields/textarea/assets/js/editor.js new file mode 100644 index 0000000..9f95dc1 --- /dev/null +++ b/panel/app/fields/textarea/assets/js/editor.js @@ -0,0 +1,64 @@ +(function($) { + + $.fn.editor = function() { + + return this.each(function() { + + if($(this).data('editor')) { + return $(this); + } + + var textarea = $(this); + var buttons = textarea.parent().find('.field-buttons'); + + // start autosizing + textarea.autosize(); + + buttons.find('.btn').on('click.editorButton', function(e) { + + textarea.focus(); + var button = $(this); + + if(button.data('action')) { + app.modal.open(button.data('action'), window.location.href); + } else { + + var sel = textarea.getSelection(); + var tpl = button.data('tpl'); + var text = button.data('text'); + + if(sel.length > 0) text = sel; + + var tag = tpl.replace('{text}', text); + + textarea.insertAtCursor(tag); + textarea.trigger('autosize.resize'); + + } + + return false; + + }); + + buttons.find('[data-editor-shortcut]').each(function(i, el) { + var key = $(this).data('editor-shortcut'); + var action = function(e) { + $(el).trigger('click'); + return false; + }; + + textarea.bind('keydown', key, action); + + if(key.match(/meta\+/)) { + textarea.bind('keydown', key.replace('meta+', 'ctrl+'), action); + } + + }); + + textarea.data('editor', true); + + }); + + }; + +})(jQuery); \ No newline at end of file diff --git a/panel/app/fields/textarea/buttons.php b/panel/app/fields/textarea/buttons.php new file mode 100644 index 0000000..c6750d8 --- /dev/null +++ b/panel/app/fields/textarea/buttons.php @@ -0,0 +1,90 @@ +textarea = $textarea; + + if(!is_array($buttons)) { + $this->buttons = array_keys(static::$setup); + } else { + $this->buttons = $buttons; + } + + } + + public function __toString() { + + $html = ''; + + return $html; + + } + +} + +buttons::$setup = array( + 'bold' => array( + 'label' => l::get('fields.textarea.buttons.bold.label'), + 'text' => l::get('fields.textarea.buttons.bold.text'), + 'shortcut' => 'meta+b', + 'template' => '**{text}**', + 'icon' => 'bold' + ), + 'italic' => array( + 'label' => l::get('fields.textarea.buttons.italic.label'), + 'text' => l::get('fields.textarea.buttons.italic.text'), + 'shortcut' => 'meta+i', + 'template' => '*{text}*', + 'icon' => 'italic' + ), + 'link' => array( + 'label' => l::get('fields.textarea.buttons.link.label'), + 'shortcut' => 'meta+shift+l', + 'action' => 'link', + 'icon' => 'chain' + ), + 'email' => array( + 'label' => l::get('fields.textarea.buttons.email.label'), + 'shortcut' => 'meta+shift+e', + 'action' => 'email', + 'icon' => 'envelope' + ), +); \ No newline at end of file diff --git a/panel/app/fields/textarea/controller.php b/panel/app/fields/textarea/controller.php new file mode 100644 index 0000000..2d3d21b --- /dev/null +++ b/panel/app/fields/textarea/controller.php @@ -0,0 +1,23 @@ +model(); + $form = $this->form('link', array($page, $this->fieldname())); + + return $this->modal('link', compact('form')); + + } + + public function email($textarea = null) { + + $page = $this->model(); + $form = $this->form('email', array($page, $this->fieldname())); + + return $this->modal('email', compact('form')); + + } + +} \ No newline at end of file diff --git a/panel/app/fields/textarea/forms/email.php b/panel/app/fields/textarea/forms/email.php new file mode 100644 index 0000000..f1e3f3c --- /dev/null +++ b/panel/app/fields/textarea/forms/email.php @@ -0,0 +1,27 @@ + array( + 'label' => 'editor.email.address.label', + 'type' => 'email', + 'placeholder' => 'editor.email.address.placeholder', + 'autofocus' => 'true', + 'required' => 'true', + ), + 'text' => array( + 'label' => 'editor.email.text.label', + 'type' => 'text', + 'help' => 'editor.email.text.help', + 'icon' => 'font' + ) + )); + + $form->data('textarea', 'form-field-' . $textarea); + $form->style('editor'); + $form->cancel($page); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/fields/textarea/forms/link.php b/panel/app/fields/textarea/forms/link.php new file mode 100644 index 0000000..27feca6 --- /dev/null +++ b/panel/app/fields/textarea/forms/link.php @@ -0,0 +1,28 @@ + array( + 'label' => 'editor.link.url.label', + 'type' => 'text', + 'placeholder' => 'http://', + 'autofocus' => 'true', + 'required' => 'true', + 'icon' => 'chain' + ), + 'text' => array( + 'label' => 'editor.link.text.label', + 'type' => 'text', + 'help' => 'editor.link.text.help', + 'icon' => 'font' + ), + )); + + $form->data('textarea', 'form-field-' . $textarea); + $form->style('editor'); + $form->cancel($page); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/fields/textarea/textarea.php b/panel/app/fields/textarea/textarea.php new file mode 100644 index 0000000..c751b8d --- /dev/null +++ b/panel/app/fields/textarea/textarea.php @@ -0,0 +1,94 @@ + array( + 'editor.js' + ) + ); + + public function __construct() { + $this->label = l::get('fields.textarea.label', 'Text'); + $this->buttons = true; + $this->min = 0; + $this->max = false; + } + + public function routes() { + return array( + array( + 'pattern' => 'link', + 'action' => 'link', + 'method' => 'get|post' + ), + array( + 'pattern' => 'email', + 'action' => 'email', + 'method' => 'get|post' + ), + ); + } + + public function input() { + + $input = parent::input(); + $input->tag('textarea'); + $input->removeAttr('type'); + $input->removeAttr('value'); + $input->html($this->value() ? htmlentities($this->value(), ENT_NOQUOTES, 'UTF-8') : false); + $input->data('field', 'editor'); + + return $input; + + } + + public function result() { + // Convert all line-endings to UNIX format + return str_replace(array("\r\n", "\r"), "\n", parent::result()); + } + + public function element() { + + $element = parent::element(); + $element->addClass('field-with-textarea'); + + if($this->buttons and !$this->readonly) { + $element->addClass('field-with-buttons'); + } + + return $element; + + } + + public function content() { + + $content = parent::content(); + + if($this->buttons and !$this->readonly) { + $content->append($this->buttons()); + } + + return $content; + + } + + public function buttons() { + require_once(__DIR__ . DS . 'buttons.php'); + return new Buttons($this, $this->buttons); + } + + public function validate() { + + if($this->validate and is_array($this->validate)) { + return parent::validate(); + } else { + if($this->min and !v::min($this->result(), $this->min)) return false; + if($this->max and !v::max($this->result(), $this->max)) return false; + } + + return true; + + } + +} diff --git a/panel/app/fields/textarea/views/email.php b/panel/app/fields/textarea/views/email.php new file mode 100644 index 0000000..df5b0a1 --- /dev/null +++ b/panel/app/fields/textarea/views/email.php @@ -0,0 +1,47 @@ + + + \ No newline at end of file diff --git a/panel/app/fields/textarea/views/link.php b/panel/app/fields/textarea/views/link.php new file mode 100644 index 0000000..a61317b --- /dev/null +++ b/panel/app/fields/textarea/views/link.php @@ -0,0 +1,53 @@ + + + \ No newline at end of file diff --git a/panel/app/fields/time/time.php b/panel/app/fields/time/time.php new file mode 100644 index 0000000..84de37f --- /dev/null +++ b/panel/app/fields/time/time.php @@ -0,0 +1,69 @@ +icon = 'clock-o'; + $this->interval = 60; + $this->format = 24; + } + + public function interval() { + if($this->interval <= 0) { + $this->interval = 60; + } + return $this->interval; + } + + public function value() { + + if($this->override()) { + $value = $this->default(); + } else { + $value = parent::value(); + } + + if(!empty($value)) { + + if($value == 'now') { + $value = date($this->format(), time()); + } + + $time = round((strtotime($value) - strtotime('00:00')) / ($this->interval() * 60)) * ($this->interval() * 60) + strtotime('00:00'); + $value = date($this->format(), $time); + + } + + return $value; + + } + + public function format() { + return $this->format == 12 ? 'h:i A' : 'H:i'; + } + + public function options() { + + $time = strtotime('00:00'); + $end = strtotime('23:59'); + $options = array(); + $format = $this->format(); + + while($time < $end) { + + $now = date($format, $time); + $time += 60 * $this->interval(); + + $options[$now] = $now; + + } + + return $options; + + } + +} diff --git a/panel/app/fields/title/title.php b/panel/app/fields/title/title.php new file mode 100644 index 0000000..236da5b --- /dev/null +++ b/panel/app/fields/title/title.php @@ -0,0 +1,40 @@ +label = l::get('fields.title.label', 'Title'); + $this->icon = 'font'; + $this->required = true; + + } + + public function help() { + + if($this->page and !$this->page->isSite()) { + + if(!empty($this->help)) { + $this->help = $this->i18n($this->help); + $this->help .= '
'; + } + + // build a readable version of the page slug + $slug = ltrim($this->page->parent()->slug() . '/', '/') . $this->page->slug(); + + // TODO: move this to the css file + $style = 'padding-left: .5rem; color: #777; border:none'; + + if($this->page->canChangeUrl()) { + $this->help .= '→' . $slug . ''; + } else { + $this->help .= '→' . $slug . ''; + } + + } + + return parent::help(); + + } + +} diff --git a/panel/app/fields/toggle/toggle.php b/panel/app/fields/toggle/toggle.php new file mode 100644 index 0000000..411524d --- /dev/null +++ b/panel/app/fields/toggle/toggle.php @@ -0,0 +1,38 @@ +text())) { + case 'yes/no': + $true = l::get('fields.toggle.yes'); + $false = l::get('fields.toggle.no'); + break; + case 'on/off': + $true = l::get('fields.toggle.on'); + $false = l::get('fields.toggle.off'); + break; + } + + return array( + 'true' => $true, + 'false' => $false + ); + + } + + public function value() { + $value = parent::value(); + + if(in_array($value, array('yes', 'true', true, 1, 'on'), true)) { + return 'true'; + } else { + return 'false'; + } + + } + +} diff --git a/panel/app/fields/url/assets/js/url.js b/panel/app/fields/url/assets/js/url.js new file mode 100644 index 0000000..600a684 --- /dev/null +++ b/panel/app/fields/url/assets/js/url.js @@ -0,0 +1,38 @@ +(function($) { + + $.fn.urlfield = function() { + + return this.each(function() { + + var $this = $(this); + + if($this.data('urlfield')) { + return; + } else { + $this.data('urlfield', true); + } + + var $icon = $this.next('.field-icon'); + + $icon.css({ + 'cursor': 'pointer', + 'pointer-events': 'auto' + }); + + $icon.on('click', function() { + + var url = $.trim($this.val()); + + if(url !== '' && $this.is(':valid')) { + window.open(url); + } else { + $this.focus(); + } + + }); + + }); + + }; + +})(jQuery); \ No newline at end of file diff --git a/panel/app/fields/url/url.php b/panel/app/fields/url/url.php new file mode 100644 index 0000000..06b07f7 --- /dev/null +++ b/panel/app/fields/url/url.php @@ -0,0 +1,30 @@ + array( + 'url.js' + ) + ); + + public function __construct() { + + $this->type = 'url'; + $this->icon = 'chain'; + $this->label = l::get('fields.url.label', 'URL'); + $this->placeholder = 'http://'; + + } + + public function validate() { + return v::url($this->value()); + } + + public function input() { + $input = parent::input(); + $input->data('field', 'urlfield'); + return $input; + } + +} \ No newline at end of file diff --git a/panel/app/fields/user/user.php b/panel/app/fields/user/user.php new file mode 100644 index 0000000..1a215ec --- /dev/null +++ b/panel/app/fields/user/user.php @@ -0,0 +1,22 @@ +type = 'text'; + $this->icon = 'user'; + $this->label = l::get('fields.user.label', 'User'); + $this->options = array(); + + foreach(kirby()->site()->users() as $user) { + $this->options[$user->username()] = $user->username(); + } + + } + + public function value() { + $value = parent::value(); + return empty($value) ? site()->user()->username() : parent::value(); + } + +} diff --git a/panel/app/forms/auth/login.php b/panel/app/forms/auth/login.php new file mode 100644 index 0000000..04622f4 --- /dev/null +++ b/panel/app/forms/auth/login.php @@ -0,0 +1,30 @@ + array( + 'label' => 'login.username.label', + 'type' => 'text', + 'icon' => 'user', + 'required' => true, + 'autofocus' => true, + 'default' => s::get('username') + ), + 'password' => array( + 'label' => 'login.password.label', + 'type' => 'password', + 'required' => true + ) + )); + + $form->attr('autocomplete', 'off'); + $form->data('autosubmit', 'native'); + $form->style('centered'); + + $form->buttons->submit->value = l('login.button'); + + return $form; + +}; + diff --git a/panel/app/forms/avatars/delete.php b/panel/app/forms/avatars/delete.php new file mode 100644 index 0000000..946337f --- /dev/null +++ b/panel/app/forms/avatars/delete.php @@ -0,0 +1,19 @@ + array( + 'type' => 'info' + ) + )); + + $form->fields->image->text = '(image: ' . $avatar->url() . ' class: avatar avatar-full avatar-centered)'; + $form->centered = true; + $form->style('delete'); + + return $form; + +}; + + diff --git a/panel/app/forms/editor/email.php b/panel/app/forms/editor/email.php new file mode 100644 index 0000000..77d0ace --- /dev/null +++ b/panel/app/forms/editor/email.php @@ -0,0 +1,26 @@ + array( + 'label' => 'editor.email.address.label', + 'type' => 'email', + 'placeholder' => 'editor.email.address.placeholder', + 'autofocus' => 'true', + 'required' => 'true', + ), + 'text' => array( + 'label' => 'editor.email.text.label', + 'type' => 'text', + 'help' => 'editor.email.text.help', + ) + )); + + $form->data('textarea', 'form-field-' . $textarea); + $form->style('editor'); + $form->cancel($page, 'show'); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/editor/link.php b/panel/app/forms/editor/link.php new file mode 100644 index 0000000..a2e5002 --- /dev/null +++ b/panel/app/forms/editor/link.php @@ -0,0 +1,26 @@ + array( + 'label' => 'editor.link.url.label', + 'type' => 'text', + 'placeholder' => 'http://', + 'autofocus' => 'true', + 'required' => 'true', + ), + 'text' => array( + 'label' => 'editor.link.text.label', + 'type' => 'text', + 'help' => 'editor.link.text.help', + ), + )); + + $form->data('textarea', 'form-field-' . $textarea); + $form->style('editor'); + $form->cancel($page, 'show'); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/files/delete.php b/panel/app/forms/files/delete.php new file mode 100644 index 0000000..e67771e --- /dev/null +++ b/panel/app/forms/files/delete.php @@ -0,0 +1,20 @@ + array( + 'label' => 'files.delete.headline', + 'type' => 'text', + 'readonly' => true, + 'icon' => false, + 'default' => $file->filename() + ) + )); + + $form->style('delete'); + $form->cancel($file); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/files/edit.php b/panel/app/forms/files/edit.php new file mode 100644 index 0000000..120f512 --- /dev/null +++ b/panel/app/forms/files/edit.php @@ -0,0 +1,47 @@ +type(); + $info[] = $file->niceSize(); + + if((string)$file->dimensions() != '0 x 0') { + $info[] = $file->dimensions(); + } + + // setup the default fields + $fields = array( + '_name' => array( + 'label' => 'files.show.name.label', + 'type' => 'filename', + 'extension' => $file->extension(), + 'required' => true, + 'default' => $file->name(), + ), + '_info' => array( + 'label' => 'files.show.info.label', + 'type' => 'text', + 'readonly' => true, + 'icon' => 'info', + 'default' => implode(' / ', $info), + ), + '_link' => array( + 'label' => 'files.show.link.label', + 'type' => 'text', + 'readonly' => true, + 'icon' => 'chain', + 'default' => $file->url() + ) + ); + + $form = new Kirby\Panel\Form(array_merge($fields, $file->getFormFields()), $file->getFormData()); + + $form->centered = true; + $form->buttons->cancel = ''; + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/installation/check.php b/panel/app/forms/installation/check.php new file mode 100644 index 0000000..ca9ff52 --- /dev/null +++ b/panel/app/forms/installation/check.php @@ -0,0 +1,35 @@ + array( + 'type' => 'info' + ) + )); + + if(count($problems) > 1) { + $info = new Brick('ol'); + foreach($problems as $problem) { + $info->append('
  • ' . $problem . '
  • '); + } + } else { + $info = new Brick('p'); + foreach($problems as $problem) { + $info->append($problem); + } + } + + // add the list of problems to the info field + $form->fields->info->text = (string)$info; + + // setup the retry button + $form->buttons->submit->value = l('installation.check.retry'); + $form->buttons->submit->autofocus = true; + + $form->style('centered'); + $form->alert(l('installation.check.text')); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/installation/signup.php b/panel/app/forms/installation/signup.php new file mode 100644 index 0000000..681931d --- /dev/null +++ b/panel/app/forms/installation/signup.php @@ -0,0 +1,58 @@ +translations() as $translation) { + $translations[$translation->code()] = $translation->title(); + } + + $form = new Kirby\Panel\Form(array( + + 'username' => array( + 'label' => 'installation.signup.username.label', + 'type' => 'text', + 'icon' => 'user', + 'placeholder' => 'installation.signup.username.placeholder', + 'required' => true, + 'autocomplete' => false, + 'autofocus' => true, + ), + + 'password' => array( + 'label' => 'installation.signup.password.label', + 'type' => 'password', + 'required' => true, + 'autocomplete' => false, + 'suggestion' => true, + ), + + 'email' => array( + 'label' => 'installation.signup.email.label', + 'placeholder' => 'installation.signup.email.placeholder', + 'type' => 'email', + 'required' => true, + 'autocomplete' => false, + ), + + 'language' => array( + 'label' => 'installation.signup.language.label', + 'type' => 'select', + 'required' => true, + 'autocomplete' => false, + 'default' => kirby()->option('panel.language', 'en'), + 'options' => $translations + ) + + )); + + $form->attr('autocomplete', 'off'); + $form->data('autosubmit', 'native'); + $form->style('centered'); + + $form->buttons->submit->value = l('installation.signup.button'); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/pages/add.php b/panel/app/forms/pages/add.php new file mode 100644 index 0000000..25a0490 --- /dev/null +++ b/panel/app/forms/pages/add.php @@ -0,0 +1,44 @@ +blueprint()->pages()->template() as $template) { + $options[$template->name()] = $template->title(); + } + + $form = new Kirby\Panel\Form(array( + 'title' => array( + 'label' => 'pages.add.title.label', + 'type' => 'title', + 'placeholder' => 'pages.add.title.placeholder', + 'autocomplete' => false, + 'autofocus' => true, + 'required' => true + ), + 'uid' => array( + 'label' => 'pages.add.url.label', + 'type' => 'text', + 'icon' => 'chain', + 'autocomplete' => false, + 'required' => true, + ), + 'template' => array( + 'label' => 'pages.add.template.label', + 'type' => 'select', + 'options' => $options, + 'default' => key($options), + 'required' => true, + 'readonly' => count($options) == 1 ? true : false, + 'icon' => count($options) == 1 ? $page->blueprint()->pages()->template()->first()->icon() : 'chevron-down', + ) + )); + + $form->cancel($page->isSite() ? '/' : $page); + + $form->buttons->submit->val(l('add')); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/pages/delete.php b/panel/app/forms/pages/delete.php new file mode 100644 index 0000000..f791f97 --- /dev/null +++ b/panel/app/forms/pages/delete.php @@ -0,0 +1,21 @@ + array( + 'label' => 'pages.delete.headline', + 'type' => 'text', + 'readonly' => true, + 'icon' => false, + 'default' => $page->title(), + 'help' => $page->id(), + ) + )); + + $form->style('delete'); + $form->cancel($page); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/pages/edit.php b/panel/app/forms/pages/edit.php new file mode 100644 index 0000000..e2359a9 --- /dev/null +++ b/panel/app/forms/pages/edit.php @@ -0,0 +1,45 @@ +getFormFields(), $page->getFormData()); + + // add the blueprint name as css class + $form->addClass('form-blueprint-' . $page->blueprint()->name()); + + // center the submit button + $form->centered = true; + + // set the keep api + $form->data('keep', $page->url('keep')); + + // set the autofocus on the title field + $form->fields->title->autofocus = true; + + // add the changes alert + if($page->changes()->differ()) { + + // display unsaved changes + $alert = new Brick('div'); + $alert->addClass('text'); + $alert->append('' . l('pages.show.changes.text') . ''); + + $form->buttons->prepend('changes', $alert); + $form->buttons->cancel->attr('href', $page->url('discard')); + $form->buttons->cancel->html(l('pages.show.changes.button')); + + // add wide buttons + $form->buttons->cancel->addClass('btn-wide'); + $form->buttons->submit->addClass('btn-wide'); + + } else { + // remove the cancel button + $form->buttons->cancel = ''; + } + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/pages/template.php b/panel/app/forms/pages/template.php new file mode 100644 index 0000000..bdaa1cc --- /dev/null +++ b/panel/app/forms/pages/template.php @@ -0,0 +1,36 @@ +parent()->blueprint()->pages()->template() as $template) { + $options[$template->name()] = $template->title(); + } + + // create the form + $form = new Kirby\Panel\Form(array( + 'template' => array( + 'label' => 'pages.template.select.label', + 'type' => 'select', + 'options' => $options, + 'default' => key($options), + 'required' => true, + 'readonly' => count($options) == 1 ? true : false, + 'icon' => count($options) == 1 ? $page->blueprint()->pages()->template()->first()->icon() : 'chevron-down', + 'autofocus' => true + ), + 'disclaimer' => array( + 'type' => 'info', + 'text' => '' + ) + ), array( + 'template' => $page->intendedTemplate() + )); + + $form->buttons->submit->val(l('change')); + $form->cancel($page); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/pages/toggle.php b/panel/app/forms/pages/toggle.php new file mode 100644 index 0000000..08cdffe --- /dev/null +++ b/panel/app/forms/pages/toggle.php @@ -0,0 +1,52 @@ +parent(); + $blueprint = $parent->blueprint(); + $siblings = $parent->children()->visible(); + + // sorting needed + if($blueprint->pages()->num()->mode() == 'default' and $siblings->count() > 0) { + + $options = array('' => l('pages.toggle.invisible'), '-' => '-'); + $n = 1; + + foreach($siblings as $sibling) { + $options[$n] = $n; + $n++; + } + + if($page->isInvisible()) { + $options[$n] = $n; + } + + $form = new Kirby\Panel\Form(array( + 'position' => array( + 'label' => l('pages.toggle.position'), + 'type' => 'select', + 'required' => true, + 'default' => $page->num(), + 'options' => $options + ) + )); + + } else { + + $form = new Kirby\Panel\Form(array( + 'confirmation' => array( + 'type' => 'info', + 'text' => $page->isVisible() ? l('pages.toggle.hide') : l('pages.toggle.publish') + ) + )); + + } + + $form->buttons->submit->value = l('change'); + $form->buttons->submit->autofocus = true; + + $form->cancel($page); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/pages/url.php b/panel/app/forms/pages/url.php new file mode 100644 index 0000000..110569e --- /dev/null +++ b/panel/app/forms/pages/url.php @@ -0,0 +1,34 @@ + 'btn btn-icon label-option', + 'href' => '#', + 'data-title' => str::slug($page->title()) + )); + + // url preview + $preview = new Brick('div', '', array('class' => 'uid-preview')); + $preview->append(ltrim($page->parent()->uri() . '/', '/')); + $preview->append('' . $page->slug() . ''); + + // create the form + $form = new Kirby\Panel\Form(array( + 'uid' => array( + 'label' => l('pages.url.uid.label') . $option, + 'type' => 'text', + 'icon' => 'chain', + 'autofocus' => true, + 'help' => $preview, + 'default' => $page->slug() + ) + )); + + $form->buttons->submit->val(l('change')); + $form->cancel($page); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/users/delete.php b/panel/app/forms/users/delete.php new file mode 100644 index 0000000..150b7ea --- /dev/null +++ b/panel/app/forms/users/delete.php @@ -0,0 +1,22 @@ + array( + 'label' => 'users.delete.headline', + 'type' => 'text', + 'readonly' => true, + 'icon' => false, + 'default' => $user->username(), + 'help' => $user->email(), + ) + )); + + $form->style('delete'); + $form->cancel($user, 'edit'); + + return $form; + +}; + diff --git a/panel/app/forms/users/user.php b/panel/app/forms/users/user.php new file mode 100644 index 0000000..5aca291 --- /dev/null +++ b/panel/app/forms/users/user.php @@ -0,0 +1,120 @@ +data() : array(); + $translations = array(); + $roles = array(); + + // make sure the password is never shown in the form + unset($content['password']); + + // add all languages + foreach(panel()->translations() as $code => $translation) { + $translations[$code] = $translation->title(); + } + + // add all roles + foreach(site()->roles() as $role) { + $roles[$role->id()] = $role->name(); + } + + // the default set of fields + $fields = array( + + 'username' => array( + 'label' => 'users.form.username.label', + 'type' => 'text', + 'icon' => 'user', + 'autofocus' => $mode != 'edit', + 'required' => true, + 'help' => $mode == 'edit' ? 'users.form.username.readonly' : 'users.form.username.help', + 'readonly' => $mode == 'edit', + ), + + 'firstName' => array( + 'label' => 'users.form.firstname.label', + 'autofocus' => $mode == 'edit', + 'type' => 'text', + 'width' => '1/2', + ), + + 'lastName' => array( + 'label' => 'users.form.lastname.label', + 'type' => 'text', + 'width' => '1/2', + ), + + 'email' => array( + 'label' => 'users.form.email.label', + 'type' => 'email', + 'required' => true, + 'autocomplete' => false + ), + + 'password' => array( + 'label' => $mode == 'edit' ? 'users.form.password.new.label' : 'users.form.password.label', + 'required' => $mode == 'add', + 'type' => 'password', + 'width' => '1/2', + 'suggestion' => true, + ), + + 'passwordConfirmation' => array( + 'label' => $mode == 'edit' ? 'users.form.password.new.confirm.label' : 'users.form.password.confirm.label', + 'required' => $mode == 'add', + 'type' => 'password', + 'width' => '1/2', + ), + + 'language' => array( + 'label' => 'users.form.language.label', + 'type' => 'select', + 'required' => true, + 'width' => '1/2', + 'default' => kirby()->option('panel.language', 'en'), + 'options' => $translations + ), + + 'role' => array( + 'label' => 'users.form.role.label', + 'type' => 'select', + 'required' => true, + 'width' => '1/2', + 'default' => site()->roles()->findDefault()->id(), + 'options' => $roles, + 'readonly' => (!panel()->user()->isAdmin() or ($user and $user->isLastAdmin())) + ), + + ); + + if($user) { + + // add all custom fields + foreach($user->blueprint()->fields()->toArray() as $name => $field) { + + if(array_key_exists($name, $fields)) { + continue; + } + + $fields[$name] = $field; + + } + + } + + // setup the form with all fields + $form = new Kirby\Panel\Form($fields, $content); + + // setup the url for the cancel button + $form->cancel('users'); + + if($mode == 'add') { + $form->buttons->submit->value = l('add'); + } + + return $form; + +}; + diff --git a/panel/app/helpers.php b/panel/app/helpers.php new file mode 100644 index 0000000..cb8bc53 --- /dev/null +++ b/panel/app/helpers.php @@ -0,0 +1,75 @@ +'; +} + +function i($icon, $position = null) { + echo icon($icon, $position); +} + +function __($var) { + echo htmlspecialchars($var); +} + +function _l($key, $default = null) { + echo htmlspecialchars(l($key, $default)); +} + +function i18n($value) { + + if(empty($value)) { + return null; + } else if(is_array($value)) { + $translation = a::get($value, panel()->translation()->code()); + + if(empty($translation)) { + // try to fallback to the default language at least + $translation = a::get($value, kirby()->option('panel.language'), $this->name()); + } + + return $translation; + } else if(is_string($value) and $translation = l::get($value)) { + return $translation; + } else { + return $value; + } + +} + +function _u($obj = '', $action = false) { + echo purl($obj, $action); +} + +function purl($obj = '/', $action = false) { + + if(empty($obj) or is_string($obj)) { + $base = panel()->urls()->index(); + return ($obj == '/' or empty($obj)) ? $base . '/' : rtrim($base . '/' . $obj, '/'); + } else if(is_a($obj, 'Kirby\\Panel\\Models\\Site')) { + return $obj->url(!$action ? 'edit' : $action); + } else if(is_a($obj, 'Kirby\\Panel\\Models\\Page')) { + return $obj->url(!$action ? 'edit' : $action); + } else if(is_a($obj, 'Kirby\\Panel\\Models\\File')) { + return $obj->url(!$action ? 'edit' : $action); + } else if(is_a($obj, 'Kirby\\Panel\\Models\\User')) { + return $obj->url(!$action ? 'edit' : $action); + } + +} + +function slugTable() { + $table = array(); + foreach(str::$ascii as $key => $value) { + $key = trim($key, '/'); + foreach(str::split($key, '|') as $needle) { + $table[$needle] = $value; + } + } + + return json_encode($table, JSON_UNESCAPED_UNICODE); +} \ No newline at end of file diff --git a/panel/app/layouts/app.php b/panel/app/layouts/app.php new file mode 100644 index 0000000..1193fcb --- /dev/null +++ b/panel/app/layouts/app.php @@ -0,0 +1,31 @@ + + + + + + + <?php __($title) ?> + + + + + + option('panel.stylesheet')): ?> + + + + + + + + + + + +
    + + +
    + + + \ No newline at end of file diff --git a/panel/app/layouts/base.php b/panel/app/layouts/base.php new file mode 100644 index 0000000..d968aff --- /dev/null +++ b/panel/app/layouts/base.php @@ -0,0 +1,21 @@ + + + + + + + <?php __($title) ?> + + + + option('panel.stylesheet')): ?> + + + + + + + + + + \ No newline at end of file diff --git a/panel/app/layouts/fatal.php b/panel/app/layouts/fatal.php new file mode 100644 index 0000000..fd6e049 --- /dev/null +++ b/panel/app/layouts/fatal.php @@ -0,0 +1,32 @@ + + + + Kirby Panel + + + + + + +

    Panel Error:

    +

    + +

    +

    + Find more info on: getkirby.com +

    + + \ No newline at end of file diff --git a/panel/app/snippets/breadcrumb.php b/panel/app/snippets/breadcrumb.php new file mode 100644 index 0000000..461820c --- /dev/null +++ b/panel/app/snippets/breadcrumb.php @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/panel/app/snippets/languages.php b/panel/app/snippets/languages.php new file mode 100644 index 0000000..d91995f --- /dev/null +++ b/panel/app/snippets/languages.php @@ -0,0 +1,21 @@ +count() > 1): ?> + +
    + + + code()) ?> + + + + +
    + + diff --git a/panel/app/snippets/menu.php b/panel/app/snippets/menu.php new file mode 100644 index 0000000..ecf362d --- /dev/null +++ b/panel/app/snippets/menu.php @@ -0,0 +1,28 @@ + + + + + \ No newline at end of file diff --git a/panel/app/snippets/meta.php b/panel/app/snippets/meta.php new file mode 100644 index 0000000..d6b4cb7 --- /dev/null +++ b/panel/app/snippets/meta.php @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/panel/app/snippets/pages/sidebar.php b/panel/app/snippets/pages/sidebar.php new file mode 100644 index 0000000..03b68ff --- /dev/null +++ b/panel/app/snippets/pages/sidebar.php @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/panel/app/snippets/pages/sidebar/file.php b/panel/app/snippets/pages/sidebar/file.php new file mode 100644 index 0000000..e7ab043 --- /dev/null +++ b/panel/app/snippets/pages/sidebar/file.php @@ -0,0 +1,6 @@ +
  • + canHavePreview(), ' data-url="' . $file->crop(75)->url() . '"') ?> data-helper="filename()) ?>" data-text="dragText()) ?>" href="url('edit')) ?>"> + icon() . __($file->filename()) ?> + + +
  • \ No newline at end of file diff --git a/panel/app/snippets/pages/sidebar/files.php b/panel/app/snippets/pages/sidebar/files.php new file mode 100644 index 0000000..bf8fbca --- /dev/null +++ b/panel/app/snippets/pages/sidebar/files.php @@ -0,0 +1,27 @@ +

    + + isSite(), l('metatags.files'), l('pages.show.files.title')) ?> + + + + + + + canHaveMoreFiles()) : ?> + + + + + + +

    + +count()): ?> + + +

    + \ No newline at end of file diff --git a/panel/app/snippets/pages/sidebar/subpage.php b/panel/app/snippets/pages/sidebar/subpage.php new file mode 100644 index 0000000..78e36e6 --- /dev/null +++ b/panel/app/snippets/pages/sidebar/subpage.php @@ -0,0 +1,7 @@ +
  • + + icon() ?>title()) ?> + displayNum()) ?> + + +
  • \ No newline at end of file diff --git a/panel/app/snippets/pages/sidebar/subpages.php b/panel/app/snippets/pages/sidebar/subpages.php new file mode 100644 index 0000000..aa9ab9d --- /dev/null +++ b/panel/app/snippets/pages/sidebar/subpages.php @@ -0,0 +1,42 @@ +

    + + + + + + + + + + modal(), ' data-modal') ?> href="url()) ?>"> + + + + + + + +

    + +count()): ?> + + + + + +

    + + + modal(), ' data-modal') ?> href="url()) ?>"> + + + + + + +

    + \ No newline at end of file diff --git a/panel/app/snippets/pagination.php b/panel/app/snippets/pagination.php new file mode 100644 index 0000000..3ffeb5a --- /dev/null +++ b/panel/app/snippets/pagination.php @@ -0,0 +1,14 @@ +pages() > 1): ?> + + \ No newline at end of file diff --git a/panel/app/snippets/search.php b/panel/app/snippets/search.php new file mode 100644 index 0000000..d7b83b4 --- /dev/null +++ b/panel/app/snippets/search.php @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/panel/app/snippets/subpages/subpage.php b/panel/app/snippets/subpages/subpage.php new file mode 100644 index 0000000..b680a91 --- /dev/null +++ b/panel/app/snippets/subpages/subpage.php @@ -0,0 +1,29 @@ +
    +
    +
    + title()) ?> +
    +
    + +
    \ No newline at end of file diff --git a/panel/app/snippets/template.php b/panel/app/snippets/template.php new file mode 100644 index 0000000..2aab7f5 --- /dev/null +++ b/panel/app/snippets/template.php @@ -0,0 +1,27 @@ + +
    +

    + + +

    + + +

    + + + +

    + + +

    + + + +

    + + +

    + + +
    + \ No newline at end of file diff --git a/panel/app/snippets/uploader.php b/panel/app/snippets/uploader.php new file mode 100644 index 0000000..ff5be50 --- /dev/null +++ b/panel/app/snippets/uploader.php @@ -0,0 +1,20 @@ + isset($multiple) ? $multiple : true, + 'accept' => isset($accept) ? $accept : false +)); + +?> + + + \ No newline at end of file diff --git a/panel/app/src/panel.php b/panel/app/src/panel.php new file mode 100644 index 0000000..7a66812 --- /dev/null +++ b/panel/app/src/panel.php @@ -0,0 +1,559 @@ + '5.4.0', + 'toolkit' => '2.2.2', + 'kirby' => '2.2.1' + ); + + static public $instance; + + public $kirby; + public $site; + public $path; + public $roots; + public $routes = array(); + public $router = null; + public $route = null; + public $translation = null; + public $translations = null; + public $csrf = null; + + static public function instance() { + return static::$instance; + } + + static public function version() { + return static::$version; + } + + public function defaults() { + + return array( + 'panel.language' => 'en', + 'panel.stylesheet' => null, + 'panel.kirbytext' => true, + 'panel.session.timeout' => 1440, + 'panel.session.lifetime' => 0, + 'panel.info.license' => true, + 'panel.info.versions' => true, + 'panel.widgets' => array( + 'pages' => true, + 'site' => true, + 'account' => true, + 'history' => true + ), + ); + + } + + public function __construct($kirby, $root) { + + // check requirements + $this->requirements(); + + // store the instance as a singleton + static::$instance = $this; + + $this->kirby = $kirby; + $this->roots = new \Kirby\Panel\Roots($this, $root); + $this->urls = new \Kirby\Panel\Urls($this, $root); + + // add the panel default options + $this->kirby->options = array_merge($this->defaults(), $this->kirby->options); + + // setup the blueprints roots + UserBlueprint::$root = $this->kirby->roots()->blueprints() . DS . 'users'; + PageBlueprint::$root = $this->kirby->roots()->blueprints(); + + // load the site object + $this->site = $this->site(); + + // setup the session + $this->session(); + + // setup the multilang site stuff + $this->multilang(); + + // load all Kirby extensions (methods, tags, smartypants) + $this->kirby->extensions(); + $this->kirby->plugins(); + + // setup the form plugin + form::$root = array( + 'default' => $this->roots->fields, + 'custom' => $this->kirby->roots()->fields() + ); + + // force ssl if set in config + if($this->kirby->option('ssl') and !r::secure()) { + // rebuild the current url with https + go(url::build(array('scheme' => 'https'))); + } + + // load all available routes + $this->routes = array_merge($this->routes, require($this->roots->config . DS . 'routes.php')); + + // start the router + $this->router = new Router($this->routes); + + // register router filters + $this->router->filter('auth', function() use($kirby) { + try { + $user = panel()->user(); + } catch(Exception $e) { + panel()->redirect('login'); + } + }); + + // check for a completed installation + $this->router->filter('isInstalled', function() use($kirby) { + $installer = new Installer(); + if(!$installer->isCompleted()) { + panel()->redirect('install'); + } + }); + + // check for valid csrf tokens. Can be used for get requests + // since all post requests are blocked anyway + $this->router->filter('csrf', function() { + panel()->csrfCheck(); + }); + + // csrf protection for every post request + if(r::is('post')) { + $this->csrfCheck(); + } + + } + + public function session() { + + // setup the session + s::$timeout = $this->kirby->option('panel.session.timeout', 120); + s::$cookie['lifetime'] = $this->kirby->option('panel.session.lifetime', 0); + + // start the session + s::start(); + + } + + public function requirements() { + + if(!version_compare(PHP_VERSION, static::$requires['php'], '>=')) { + throw new Exception('Your PHP version is too old. Please upgrade to ' . static::$requires['php'] . ' or newer.'); + } + + if(!detect::mbstring()) { + throw new Exception('The mbstring extension must be installed'); + } + + if(!version_compare(toolkit::version(), static::$requires['toolkit'], '>=')) { + throw new Exception('Your Toolkit version is too old. Please upgrade to ' . static::$requires['toolkit'] . ' or newer.'); + } + + if(!version_compare(kirby::version(), static::$requires['kirby'], '>=')) { + throw new Exception('Your Kirby version is too old. Please upgrade to ' . static::$requires['kirby'] . ' or newer.'); + } + + } + + public function csrf() { + + if(!is_null($this->csrf)) return $this->csrf; + + // see if there's a token in the session + $token = s::get('csrf'); + + // create a new csrf token if not available yet + if(str::length($token) !== 32) { + $token = str::random(32); + } + + // store the new token in the session + s::set('csrf', $token); + + // create a new csrf token + return $this->csrf = $token; + + } + + public function csrfCheck() { + + $csrf = get('csrf'); + + if(empty($csrf) or $csrf !== s::get('csrf')) { + + try { + $this->user()->logout(); + } catch(Exception $e) {} + + $this->redirect('login'); + + } + + } + + public function kirby() { + return $this->kirby; + } + + public function site() { + + // return the site object if it has already been stored + if(!is_null($this->site)) return $this->site; + + // load the original site first to load all branch files + $this->kirby->site(); + + // create a new panel site object + return $this->site = new Site($this->kirby); + + } + + public function multilang() { + + if(!$this->site->multilang()) { + $language = null; + } else if($language = get('language') or $language = s::get('lang')) { + // $language is already set + } else { + $language = null; + } + + // set the path and lang for the original site object + $this->kirby->site()->visit('/', $language); + + // set the path and lang for the panel site object + $this->site->visit('/', $language); + + // store the language code + if($this->site->multilang()) { + s::set('lang', $this->site->language()->code()); + } + + } + + public function page($id) { + if($page = (empty($id) or $id == '/') ? $this->site() : $this->site()->find($id)) { + return $page; + } else { + throw new Exception(l('pages.error.missing')); + } + } + + public function roots() { + return $this->roots; + } + + public function routes($routes = null) { + if(is_null($routes)) return $this->routes; + return $this->routes = array_merge($this->routes, (array)$routes); + } + + public function urls() { + return $this->urls; + } + + public function form($id, $data = array(), $submit = null) { + + if(file_exists($id)) { + $file = $id; + } else { + $file = $this->roots->forms . DS . $id . '.php'; + } + + if(!file_exists($file)) { + throw new Exception(l('form.error.missing')); + } + + $callback = require($file); + + if(!is_callable($callback)) { + throw new Exception(l('form.construct.error.invalid')); + } + + $form = call($callback, $data); + + if(is_callable($submit)) { + $form->on('submit', $submit); + } + + return $form; + + } + + public function translations() { + + if(!is_null($this->translations)) return $this->translations; + + $this->translations = new Collection; + + foreach(dir::read($this->roots()->translations()) as $dir) { + // filter out everything but directories + if(!is_dir($this->roots()->translations() . DS . $dir)) continue; + + // create the translation object + $translation = new Translation($this, $dir); + $this->translations->append($translation->code(), $translation); + } + + return $this->translations; + + } + + public function translation() { + + if(!is_null($this->translation)) return $this->translation; + + // get the default language code from the options + $lang = $this->kirby()->option('panel.language', 'en'); + $user = $this->site()->user(); + + if($user && $user->language()) { + $lang = $user->language(); + } + + return $this->translation = new Translation($this, $lang); + + } + + public function language() { + return $this->translation; + } + + public function direction() { + return $this->translation->direction(); + } + + public function launch($path = null) { + + // set the timezone for all date functions + date_default_timezone_set($this->kirby->options['timezone']); + + // load the current translation + $this->translation()->load(); + + $this->path = $this->kirby->path(); + $this->route = $this->router->run($this->path); + + // set the current url + $this->urls->current = rtrim($this->urls->index() . '/' . $this->path, '/'); + + ob_start(); + + try { + + // react on invalid routes + if(!$this->route) { + throw new Exception(l('routes.error.invalid')); + } + + if(is_callable($this->route->action())) { + $response = call($this->route->action(), $this->route->arguments()); + } else { + $response = $this->response(); + } + + } catch(Exception $e) { + require_once($this->roots->controllers . DS . 'error.php'); + $controller = new ErrorController(); + $response = $controller->index($e->getMessage(), $e); + } + + // check for a valid response object + if(is_a($response, 'Response')) { + echo $response; + } else { + echo new Response($response); + } + + ob_end_flush(); + + } + + public function response() { + + // let's find the controller and controller action + $controllerParts = str::split($this->route->action(), '::'); + $controllerUri = $controllerParts[0]; + $controllerAction = $controllerParts[1]; + $controllerFile = $this->roots->controllers . DS . strtolower(str_replace('Controller', '', $controllerUri)) . '.php'; + $controllerName = basename($controllerUri); + + // react on missing controllers + if(!file_exists($controllerFile)) { + throw new Exception(l('controller.error.invalid')); + } + + // load the controller + require_once($controllerFile); + + // check for the called action + if(!method_exists($controllerName, $controllerAction)) { + throw new Exception(l('controller.error.action')); + } + + // run the controller + $controller = new $controllerName; + + // call the action and pass all arguments from the router + return call(array($controller, $controllerAction), $this->route->arguments()); + + } + + public function license() { + + $key = c::get('license'); + $type = 'trial'; + + /** + * Hey stranger, + * + * So this is the mysterious place where the panel checks for + * valid licenses. As you can see, this is not reporting + * back to any server and the license keys are rather simple to + * hack. If you really feel like removing the warning in the panel + * or tricking Kirby into believing you bought a valid license even + * if you didn't, go for it! But remember that literally thousands of + * hours of work have gone into Kirby in order to make your + * life as a developer, designer, publisher, etc. easier. If this + * doesn't mean anything to you, you are probably a lost case anyway. + * + * Have a great day! + * + * Bastian + */ + if(str::startsWith($key, 'K2-PRO') and str::length($key) == 39) { + $type = 'Kirby 2 Professional'; + } else if(str::startsWith($key, 'K2-PERSONAL') and str::length($key) == 44) { + $type = 'Kirby 2 Personal'; + } else if(str::startsWith($key, 'MD-') and str::length($key) == 35) { + $type = 'Kirby 1'; + } else if(str::startsWith($key, 'BETA') and str::length($key) == 9) { + $type = 'Kirby 1'; + } else if(str::length($key) == 32) { + $type = 'Kirby 1'; + } else { + $key = null; + } + + return new Obj(array( + 'key' => $key, + 'local' => $this->isLocal(), + 'type' => $type, + )); + + } + + public function isLocal() { + $localhosts = array('::1', '127.0.0.1', '0.0.0.0'); + return (in_array(server::get('SERVER_ADDR'), $localhosts) || server::get('SERVER_NAME') == 'localhost'); + } + + public function notify($text) { + s::set('message', array( + 'type' => 'notification', + 'text' => $text, + )); + } + + public function alert($text) { + s::set('message', array( + 'type' => 'error', + 'text' => $text, + )); + } + + public function redirect($obj = '/', $action = false, $force = false) { + + if($force === false and $redirect = get('_redirect')) { + $url = purl($redirect); + } else { + $url = purl($obj, $action); + } + + if(r::ajax()) { + + $user = $this->site()->user(); + + die(response::json(array( + 'direction' => $this->direction(), + 'user' => $user ? $user->username() : false, + 'url' => $url + ))); + + } else { + go($url); + } + + } + + public function users() { + return $this->site()->users(); + } + + public function user($username = null) { + if($user = $this->site()->user($username)) { + return $user; + } else { + throw new Exception(l('users.error.missing')); + } + } + + public static function fatal($e, $root) { + + $message = $e->getMessage() ? $e->getMessage() : 'Error without a useful message :('; + $where = implode('
    ', [ + '', + '', + 'It happened here:', + 'File: ' . str_replace($root, '/panel', $e->getFile()) . '', + 'Line: ' . $e->getLine() . '' + ]); + + // load the fatal screen + return tpl::load($root . DS . 'app' . DS . 'layouts' . DS . 'fatal.php', [ + 'css' => url::index() . '/assets/css/panel.css', + 'content' => $message . $where + ]); + + } + +} diff --git a/panel/app/src/panel/autocomplete.php b/panel/app/src/panel/autocomplete.php new file mode 100644 index 0000000..2460759 --- /dev/null +++ b/panel/app/src/panel/autocomplete.php @@ -0,0 +1,119 @@ +panel = $panel; + $this->site = $panel->site(); + $this->method = $method; + $this->params = $params; + } + + public function result() { + + $method = 'autocomplete' . $this->method; + + if(!method_exists($this, $method)) { + throw new Exception(l('autocomplete.method.error')); + } + + $result = array_values((array)$this->$method($this->params)); + + // sort results alphabetically + sort($result); + + return $result; + + } + + public function autocompleteUsernames() { + return $this->panel->users()->map(function($user) { + return $user->username(); + })->toArray(); + } + + public function autocompleteEmails() { + return $this->panel->users()->map(function($user) { + return $user->email(); + })->toArray(); + } + + public function autocompleteUris() { + return $this->site->index()->map(function($page) { + return $page->id(); + })->toArray(); + } + + public function autocompleteField($params = array()) { + + $defaults = array( + 'index' => 'siblings', + 'uri' => '/', + 'field' => 'tags', + 'yaml' => false, + 'model' => 'page', + 'separator' => true + ); + + $options = array_merge($defaults, $params); + $page = $this->panel->page($options['uri']); + $pages = $this->pages($page, $options['index'], $options); + $yaml = $options['yaml']; + + if($yaml or $options['model'] == 'file') { + $result = array(); + foreach($pages as $p) { + if($yaml) { + $index = $p->$yaml()->toStructure(); + } elseif($options['model'] == 'file') { + $index = $p->files(); + } + $values = $index->pluck($options['field'], $options['separator'], true); + $result = array_merge($result, $values); + } + $result = array_unique($result); + } else { + $result = $pages->pluck($options['field'], $options['separator'], true); + } + + return $result; + + } + + public function pages($page, $index, $params = array()) { + + switch($index) { + case 'self': + return new Pages(array($page)); + break; + case 'siblings': + case 'children': + return $page->$index(); + break; + case 'template': + $template = a::get($params, 'template', $page->template()); + return $this->site->index()->filterBy('template', $template); + break; + case 'pages': + case 'all': + return $this->site->index(); + break; + default: + return $page->children(); + break; + } + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/collections/children.php b/panel/app/src/panel/collections/children.php new file mode 100644 index 0000000..6b1ee83 --- /dev/null +++ b/panel/app/src/panel/collections/children.php @@ -0,0 +1,129 @@ +reset(); + + $inventory = $page->inventory(); + + foreach($inventory['children'] as $dirname) { + $child = new Page($page, $dirname); + $this->data[$child->id()] = $child; + } + + $sort = $page->blueprint()->pages()->sort(); + + switch($sort) { + case 'flip': + $cloned = $this->flip(); + $this->data = $cloned->data; + break; + default; + $parts = str::split($sort, ' '); + if(count($parts) > 0) { + $cloned = call(array($this, 'sortBy'), $parts); + $this->data = $cloned->data; + } + break; + } + + } + + public function create($uid, $template, $content = array()) { + + if(empty($template)) { + throw new Exception(l('pages.add.error.template')); + } + + $uid = empty($uid) ? str::random(32) : $uid; + $blueprint = new Blueprint($template); + $data = array(); + + foreach($blueprint->fields(null) as $key => $field) { + $data[$key] = $field->default(); + } + + $data = array_merge($data, $content); + + // create the new page and convert it to a page model + $page = new Page($this->page, parent::create($uid, $template, $data)->dirname()); + + if(!$page) { + throw new Exception(l('pages.add.error.create')); + } + + kirby()->trigger('panel.page.create', $page); + + // subpage builder + foreach((array)$page->blueprint()->pages()->build() as $build) { + $missing = a::missing($build, array('title', 'template', 'uid')); + if(!empty($missing)) continue; + $subpage = $page->children()->create($build['uid'], $build['template'], array('title' => $build['title'])); + if(isset($build['num'])) $subpage->sort($build['num']); + } + + return $page; + + } + + public function paginated($mode = 'sidebar') { + + if($limit = $this->page->blueprint()->pages()->limit()) { + + $hash = sha1($this->page->id()); + + switch($mode) { + case 'sidebar': + $id = 'pages.' . $hash; + $var = 'page'; + break; + case 'subpages/visible': + $id = 'subpages.visible.' . $hash; + $var = 'visible'; + break; + case 'subpages/invisible': + $id = 'subpages.invisible.' . $hash; + $var = 'invisible'; + break; + } + + // filter out hidden pages + $children = $this->filter(function($child) { + return $child->blueprint()->hide() === false; + }); + + $children = $children->paginate($limit, array( + 'page' => get($var, s::get($id)), + 'omitFirstPage' => false, + 'variable' => $var, + 'method' => 'query', + 'redirect' => false + )); + + // store the last page + s::set($id, $children->pagination()->page()); + + return $children; + + } else { + return $this; + } + + } + + +} diff --git a/panel/app/src/panel/collections/files.php b/panel/app/src/panel/collections/files.php new file mode 100644 index 0000000..777590c --- /dev/null +++ b/panel/app/src/panel/collections/files.php @@ -0,0 +1,51 @@ +kirby = $page->kirby; + $this->site = $page->site; + $this->page = $page; + + // make sure the inventory is always fresh + $this->page->reset(); + + $inventory = $page->inventory(); + + foreach($inventory['files'] as $filename) { + $file = new File($this, $filename); + $this->data[strtolower($file->filename())] = $file; + } + + if($this->page->canSortFiles()) { + $sorted = $this->sortBy('sort', 'asc'); + $this->data = $sorted->data; + } + + if($this->page->blueprint()->files()->sort() == 'flip') { + $flipped = $this->flip(); + $this->data = $flipped->data; + } + + } + + public function topbar($topbar) { + + $page = $this->page(); + + if($page->isSite()) { + $topbar->append(purl('options'), l('metatags')); + } + + $page->topbar($topbar); + + $topbar->append($page->url('files'), l('files')); + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/collections/users.php b/panel/app/src/panel/collections/users.php new file mode 100644 index 0000000..e9f819a --- /dev/null +++ b/panel/app/src/panel/collections/users.php @@ -0,0 +1,39 @@ +map(function($user) { + return new User($user->username()); + }); + + } + + public function topbar($topbar) { + $topbar->append(purl('users'), l('users')); + } + + public function create($data) { + + if($data['password'] !== $data['passwordconfirmation']) { + throw new Exception(l('users.form.error.password.confirm')); + } + + unset($data['passwordconfirmation']); + + $user = parent::create($data); + kirby()->trigger('panel.user.create', $user); + return new User($user->username()); + + } + + +} \ No newline at end of file diff --git a/panel/app/src/panel/controllers/base.php b/panel/app/src/panel/controllers/base.php new file mode 100644 index 0000000..1c6b864 --- /dev/null +++ b/panel/app/src/panel/controllers/base.php @@ -0,0 +1,121 @@ +redirect($obj, $action, $force); + } + + public function notify($message) { + panel()->notify($message); + } + + public function alert($message) { + panel()->alert($message); + } + + public function form($id, $data = array(), $submit = null) { + return panel()->form($id, $data, $submit); + } + + public function page($id) { + return panel()->page($id); + } + + public function user($username = null) { + return panel()->user($username); + } + + public function layout($type, $data = array()) { + + $version = panel()->version(); + $base = panel()->urls()->index(); + $cssbase = panel()->urls()->css(); + $jsbase = panel()->urls()->js(); + + $defaults = array( + 'title' => panel()->site()->title() . ' | Panel', + 'direction' => panel()->direction(), + 'meta' => $this->snippet('meta'), + 'css' => css($cssbase . '/panel.min.css?v=' . $version), + 'js' => js($jsbase . '/dist/panel.min.js?v=' . $version), + 'content' => '', + 'bodyclass' => '', + ); + + switch($type) { + case 'app': + $defaults['topbar'] = ''; + $defaults['csrf'] = panel()->csrf(); + $defaults['formcss'] = css($cssbase . '/form.min.css?v=' . $version); + $defaults['formjs'] = js($jsbase . '/dist/form.min.js?v=' . $version); + $defaults['appjs'] = js($jsbase . '/dist/app.min.js?v=' . $version); + + // plugin stuff + $defaults['pluginscss'] = css($base . '/plugins/css?v=' . $version); + $defaults['pluginsjs'] = js($base . '/plugins/js?v=' . $version); + + break; + case 'base': + break; + } + + $data = array_merge($defaults, $data); + + if(r::ajax() and $type == 'app') { + $panel = panel(); + $user = $panel->site()->user(); + $response = array( + 'user' => $user ? $user->username() : false, + 'direction' => $panel->direction(), + 'title' => $data['title'], + 'content' => $data['topbar'] . $data['content'] + ); + return response::json($response); + } else { + return new Layout($type, $data); + } + + } + + public function view($file, $data = array()) { + return new View($file, $data); + } + + public function snippet($file, $data = array()) { + return new Snippet($file, $data); + } + + public function topbar($view, $input) { + return new Topbar($view, $input); + } + + public function screen($view, $topbar = null, $data = array()) { + return $this->layout('app', array( + 'topbar' => is_a($topbar, 'Kirby\\Panel\\Topbar') ? $topbar : $this->topbar($view, $topbar), + 'content' => is_a($data, 'Kirby\\Panel\\View') ? $data : $this->view($view, $data) + )); + } + + public function modal($view, $data = array()) { + if($view === 'error') $view = 'error/modal'; + return $this->layout('app', array('content' => $this->view($view, $data))); + } + + public function json($data = array()) { + return response::json($data); + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/controllers/field.php b/panel/app/src/panel/controllers/field.php new file mode 100644 index 0000000..53073fb --- /dev/null +++ b/panel/app/src/panel/controllers/field.php @@ -0,0 +1,47 @@ +model = $model; + $this->field = $field; + $this->fieldname = $field->name(); + } + + public function form($id, $data = array(), $submit = null) { + $file = $this->field->root() . DS . 'forms' . DS . $id . '.php'; + return panel()->form($file, $data, $submit); + } + + public function view($file, $data = array()) { + + $view = new View($file, $data); + $root = $this->field->root() . DS . 'views'; + + if(file_exists($root . DS . $file . '.php')) { + $view->_root = $root; + } + + return $view; + + } + + public function snippet($file, $data = array()) { + + $snippet = new Snippet($file, $data); + $root = $this->field->root() . DS . 'snippets'; + + if(file_exists($root . DS . $file . '.php')) { + $snippet->_root = $root; + } + + return $snippet; + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/form.php b/panel/app/src/panel/form.php new file mode 100644 index 0000000..e489deb --- /dev/null +++ b/panel/app/src/panel/form.php @@ -0,0 +1,340 @@ +fields = new Collection; + + // if Form is part of a structureField, set structureField name + $this->parentField = $parent; + + // initialize all field plugins + $this->plugins = new Plugins(); + + $this->values($values); + $this->fields($fields); + $this->buttons(); + $this->attr('method', 'post'); + $this->attr('action', panel()->urls()->current()); + $this->addClass('form'); + + } + + public function method($method = null) { + return $this->attr('method', $method); + } + + public function action($action = null) { + return $this->attr('action', $action); + } + + public function fields($fields = null) { + + if(is_null($fields)) return $this->fields; + + // get the site object + $site = panel()->site(); + + // check if untranslatable fields should be deactivated + $translated = $site->multilang() && !$site->language()->default(); + + foreach($fields as $name => $field) { + + $name = str_replace('-','_', str::lower($name)); + + $field['name'] = $name; + $field['default'] = a::get($field, 'default', null); + $field['value'] = a::get($this->values(), $name, $field['default']); + + // Pass through parent field name (structureField) + $field['parentField'] = $this->parentField; + + // Check for untranslatable fields + if($translated and isset($field['translate']) and $field['translate'] === false) { + $field['readonly'] = true; + $field['disabled'] = true; + } + + $this->fields->append($name, static::field($field['type'], $field)); + + } + + return $this; + + } + + public function values($values = null) { + if(is_null($values)) return array_merge($this->values, r::data()); + $this->values = array_merge($this->values, $values); + return $this; + } + + public function value($name) { + return a::get($this->values(), $name, null); + } + + public function validate() { + + $site = panel()->site(); + $translated = $site->multilang() && !$site->language()->default(); + $errors = array(); + + foreach($this->fields() as $field) { + + // don't validate fields, which are not translatable + if($translated and $field->translate() === false) continue; + + $name = $field->name(); + $value = $this->value($name); + + if($field->required() and $value == '') { + $field->error = true; + } else if($value !== '' and $field->validate() == false) { + $field->error = true; + } + + } + + } + + public function isValid() { + return $this->fields()->filterBy('error', true)->count() == 0; + } + + public function message($type, $text) { + + $this->message = new Brick('div'); + $this->message->addClass('message'); + + if($type == 'error') { + $this->message->addClass('message-is-alert'); + } else { + $this->message->addClass('message-is-notice'); + } + + $this->message->append(function() use($text) { + + $content = new Brick('span'); + $content->addClass('message-content'); + $content->text($text); + + return $content; + + }); + + return $this->message; + + } + + public function alert($text) { + $this->message('error', $text); + } + + public function notify($text) { + $this->message('success', $text); + } + + public function serialize() { + + $data = array(); + $site = panel()->site(); + $fields = $this->fields(); + + foreach($fields as $field) { + $result = $field->result(); + if(!is_null($result)) $data[$field->name()] = $result; + } + + // unset untranslatable fields in all languages but the default lang + if($site->multilang() and $site->language() != $site->defaultLanguage()) { + foreach($fields as $field) { + if($field->translate() === false) { + $data[$field->name()] = null; + } + } + } + + return $data; + + } + + public function toArray() { + return $this->serialize(); + } + + public function plugins() { + return $this->plugins; + } + + public function style($style) { + + switch($style) { + case 'centered': + $this->centered = true; + $this->buttons->cancel = ''; + break; + case 'upload': + $this->centered = true; + $this->buttons->submit = ''; + $this->attr('enctype', 'multipart/form-data'); + break; + case 'delete': + $this->buttons->submit->addClass('btn-negative'); + $this->buttons->submit->attr('autofocus', true); + $this->buttons->submit->val(l('delete')); + break; + case 'editor': + + $kirbytext = kirby()->option('panel.kirbytext', true); + + $this->data('textarea', get('textarea')); + $this->data('autosubmit', 'false'); + $this->data('kirbytext', r($kirbytext, 'true', 'false')); + $this->buttons->submit->val(l('insert')); + break; + } + + } + + public function redirect() { + return get('_redirect'); + } + + public function cancel() { + if($redirect = $this->redirect()) { + $this->buttons->cancel->href = purl($redirect); + } else { + $this->buttons->cancel->href = call('purl', func_get_args()); + } + } + + static public function field($type, $options = array()) { + + $class = $type . 'field'; + + if(!class_exists($class)) { + throw new Exception('The ' . $type . ' field is missing. Please add it to your installed fields or remove it from your blueprint'); + } + + $field = new $class; + + foreach($options as $key => $value) { + $field->$key = $value; + } + + return $field; + + } + + public function buttons() { + + if(!is_null($this->buttons)) return $this->buttons; + + $this->buttons = new Collection(); + + $button = new Brick('input', null); + $button->addClass('btn btn-rounded'); + + $cancel = clone $button; + $cancel->tag('a'); + $cancel->addClass('btn-cancel'); + $cancel->attr('href', '#cancel'); + $cancel->text(l('cancel')); + + $this->buttons->append('cancel', $cancel); + + $submit = clone $button; + $submit->attr('type', 'submit'); + $submit->addClass('btn-submit'); + $submit->data('saved', l('saved')); + $submit->val(l('save')); + + $this->buttons->append('submit', $submit); + + return $this->buttons; + + } + + public function on($action, $callback) { + + // auto-trigger the submit event when the form is being echoed + if(r::is('post')) { + $callback($this); + } + + $this->fields->append('csrf', static::field('hidden', array( + 'name' => 'csrf', + 'value' => panel()->csrf() + ))); + + } + + public function toHTML() { + + if($this->message) { + $this->append($this->message); + } + + $fieldset = new Brick('fieldset'); + $fieldset->addClass('fieldset field-grid cf'); + + foreach($this->fields() as $field) $fieldset->append($field); + + // pass the redirect url + $redirectField = new Brick('input'); + $redirectField->type = 'hidden'; + $redirectField->name = '_redirect'; + $redirectField->value = $this->redirect(); + $fieldset->append($redirectField); + + $this->append($fieldset); + + $buttons = new Brick('fieldset'); + $buttons->addClass('fieldset buttons'); + + if($this->centered) { + $buttons->addClass('buttons-centered'); + } + + foreach($this->buttons() as $button) $buttons->append($button); + + $this->append($buttons); + + return $this; + + } + + public function __toString() { + + $this->toHTML(); + return parent::__toString(); + + } + +} diff --git a/panel/app/src/panel/form/fieldoptions.php b/panel/app/src/panel/form/fieldoptions.php new file mode 100644 index 0000000..fb17ace --- /dev/null +++ b/panel/app/src/panel/form/fieldoptions.php @@ -0,0 +1,256 @@ +toArray(); + } + + public function __construct($field) { + + $this->field = $field; + + if(is_array($this->field->options)) { + $this->options = $this->field->options; + } else if($this->isUrl($this->field->options)) { + $this->options = $this->optionsFromApi($this->field->options); + } else if($this->field->options == 'query') { + $this->options = $this->optionsFromQuery($this->field->query); + } else if($this->field->options == 'field') { + $this->options = $this->optionsFromField($this->field->field); + } else { + $this->options = $this->optionsFromPageMethod($this->field->page, $this->field->options); + } + + // sorting + $this->options = $this->sort($this->options, !empty($this->field->sort) ? $this->field->sort : null); + + } + + public function optionsFromPageMethod($page, $method) { + + if($page && $items = $this->items($page, $method)) { + $options = array(); + foreach($items as $item) { + if(is_a($item, 'Page')) { + $options[$item->uid()] = (string)$item->title(); + } else if(is_a($item, 'File')) { + $options[$item->filename()] = (string)$item->filename(); + } + } + return $options; + } else { + return array(); + } + + } + + public function optionsFromApi($url) { + $response = remote::get($url); + $options = @json_decode($response->content(), true); + return is_array($options) ? $options : array(); + } + + public function optionsFromField($field) { + + // default field parameters + $defaults = array( + 'page' => $this->field->page ? ($this->field->page->isSite() ? '/' : $this->field->page->id()) : '', + 'name' => 'tags', + 'separator' => ',', + ); + + // sanitize the query + if(!is_array($field)) { + $field = array(); + } + + // merge the default parameters with the actual query + $field = array_merge($defaults, $field); + + // dynamic page option + // ../ + // ../../ etc. + $page = $this->page($field['page']); + $items = $page->{$field['name']}()->split($field['separator']); + $options = array(); + + foreach($items as $item) { + $options[$item] = $item; + } + + return $options; + + } + + public function optionsFromQuery($query) { + + // default query parameters + $defaults = array( + 'page' => $this->field->page ? ($this->field->page->isSite() ? '/' : $this->field->page->id()) : '', + 'fetch' => 'children', + 'value' => '{{uid}}', + 'text' => '{{title}}', + 'flip' => false, + 'template' => false + ); + + // sanitize the query + if(!is_array($query)) { + $query = array(); + } + + // merge the default parameters with the actual query + $query = array_merge($defaults, $query); + + // dynamic page option + // ../ + // ../../ etc. + $page = $this->page($query['page']); + $items = $this->items($page, $query['fetch']); + $options = array(); + + if($query['template']) { + $items = $items->filter(function($item) use($query) { + return in_array(str::lower($item->intendedTemplate()), array_map('str::lower', (array)$query['template'])); + }); + } + + if($query['flip']) { + $items = $items->flip(); + } + + foreach($items as $item) { + $value = $this->tpl($query['value'], $item); + $text = $this->tpl($query['text'], $item); + + $options[$value] = $text; + } + + return $options; + + } + + public function page($uri) { + + if(str::startsWith($uri, '../')) { + if($currentPage = $this->field->page) { + $path = $uri; + while(str::startsWith($path, '../')) { + if($parent = $currentPage->parent()) { + $currentPage = $parent; + } else { + $currentPage = site(); + } + $path = str::substr($path, 3); + } + if(!empty($path)) { + $currentPage = $currentPage->find($path); + } + $page = $currentPage; + } else { + $page = null; + } + } else if($uri == '/') { + $page = site(); + } else { + $page = page($uri); + } + + return $page; + + } + + public function sort($options, $sort) { + + if(empty($sort)) return $options; + + switch(strtolower($sort)) { + case 'asc': + asort($options); + break; + case 'desc': + arsort($options); + break; + } + + return $options; + + } + + public function tpl($string, $obj) { + return preg_replace_callback('!\{\{(.*?)\}\}!', function($item) use($obj) { + return (string)$obj->{$item[1]}(); + }, $string); + } + + public function isUrl($url) { + return + v::url($url) or + str::contains($url, '://localhost') or + str::contains($url, '://127.0.0.1'); + } + + public function items($page, $method) { + + if(!$page) return new Collection(); + + switch($method) { + case 'visibleChildren': + $items = $page->children()->visible(); + break; + case 'invisibleChildren': + $items = $page->children()->invisible(); + break; + case 'siblings': + $items = $page->siblings()->not($page); + break; + case 'visibleSiblings': + $items = $page->siblings()->not($page)->visible(); + break; + case 'invisibleSiblings': + $items = $page->siblings()->not($page)->invisible(); + break; + case 'pages': + $items = site()->index(); + $items = $items->sortBy('title', 'asc'); + break; + case 'index': + $items = $page->index(); + $items = $items->sortBy('title', 'asc'); + break; + case 'children': + case 'grandchildren': + case 'files': + case 'images': + case 'documents': + case 'videos': + case 'audio': + case 'code': + case 'archives': + $items = $page->{$method}(); + break; + default: + $items = new Collection(); + } + + return $items; + + } + + public function toArray() { + return $this->options; + } + +} diff --git a/panel/app/src/panel/form/plugins.php b/panel/app/src/panel/form/plugins.php new file mode 100644 index 0000000..4ff4e42 --- /dev/null +++ b/panel/app/src/panel/form/plugins.php @@ -0,0 +1,113 @@ +find(); + $this->load(); + } + + public function find() { + + $kirby = kirby(); + + // store all fields coming from plugins and load + // them between the default fields and the custom fields + $pluginfields = $kirby->get('field'); + + // load the default panel fields first, because they can be overwritten + foreach(dir::read(form::$root['default']) as $name) { + $kirby->set('field', $name, form::$root['default'] . DS . $name); + } + + // load the plugin fields again. A bit hacky, but works + foreach($pluginfields as $name => $field) { + $kirby->set('field', $name, $field->root()); + } + + // load all custom fields, which can overwrite all the others + foreach(dir::read(form::$root['custom']) as $name) { + $kirby->set('field', $name, form::$root['custom'] . DS . $name); + } + + } + + public function load() { + + $fields = kirby()->get('field'); + $classes = []; + + foreach($fields as $name => $field) { + $classes[$field->class()] = $field->file(); + } + + // start the autoloader + load($classes); + + foreach($fields as $name => $field) { + + $classname = $field->class(); + + if(!class_exists($classname)) { + throw new Exception('The field class is missing for: ' . $classname); + } + + if(method_exists($classname, 'setup')) { + call(array($classname, 'setup')); + } + + } + + } + + public function assets($type) { + + $output = []; + $defaultRoot = panel()->roots()->fields(); + + foreach(kirby()->get('field') as $name => $field) { + + $root = $field->root(); + $base = dirname($root); + + // only fetch assets for custom fields + if($base == $defaultRoot) { + continue; + } + + $classname = $field->class(); + + if(!class_exists($classname)) { + throw new Exception('The field class is missing for: ' . $classname); + } + + if(!isset($classname::$assets) || !isset($classname::$assets[$type])) { + continue; + } + + foreach($classname::$assets[$type] as $filename) { + $output[] = f::read($field->root() . DS . 'assets' . DS . $type . DS . $filename); + } + + } + + return implode(PHP_EOL . PHP_EOL, $output); + + } + + public function css() { + return $this->assets('css'); + } + + public function js() { + return $this->assets('js'); + } + +} diff --git a/panel/app/src/panel/installer.php b/panel/app/src/panel/installer.php new file mode 100644 index 0000000..e0ca048 --- /dev/null +++ b/panel/app/src/panel/installer.php @@ -0,0 +1,78 @@ +users()->count() > 0 && is_writable(kirby()->roots()->accounts())); + } + + public function problems() { + + $checks = array('allowed', 'accounts', 'thumbs', 'blueprints', 'content', 'avatars'); + $problems = array(); + + foreach($checks as $c) { + $method = 'check' . $c; + + if(!$this->$method()) { + $problems[] = l('installation.check.error.' . $c); + } + + } + + return empty($problems) ? false : $problems; + + } + + protected function checkAllowed() { + return (panel()->isLocal() || kirby()->option('panel.install') === true); + } + + protected function checkAccounts() { + + $root = kirby()->roots()->accounts(); + + // try to create the accounts folder + dir::make($root); + + return is_writable($root); + + } + + protected function checkThumbs() { + + $root = kirby()->roots()->thumbs(); + + // try to create the thumbs folder + dir::make($root); + + return is_writable($root); + + } + + protected function checkBlueprints() { + return is_dir(kirby()->roots()->blueprints()); + } + + protected function checkContent() { + $folder = new Folder(kirby()->roots()->content()); + return $folder->isWritable(true); + } + + protected function checkAvatars() { + + $root = kirby()->roots()->avatars(); + + // try to create the avatars folder + dir::make($root); + + return is_writable($root); + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/layout.php b/panel/app/src/panel/layout.php new file mode 100644 index 0000000..1aea276 --- /dev/null +++ b/panel/app/src/panel/layout.php @@ -0,0 +1,14 @@ +_root = panel::instance()->roots()->layouts(); + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/login.php b/panel/app/src/panel/login.php new file mode 100644 index 0000000..5f816e3 --- /dev/null +++ b/panel/app/src/panel/login.php @@ -0,0 +1,246 @@ +kirby = kirby(); + $this->logfile = $this->kirby->roots()->accounts() . DS . '.logins'; + + $this->setup(); + + } + + /** + * Setup and check the logfile + */ + protected function setup() { + + // make sure the logroot exists + if(!is_writable(dirname($this->logfile))) { + throw new Exception(l('users.form.error.permissions.title')); + } + + // create the logfile if not there yet + touch($this->logfile); + + // make sure the logroot exists + if(!is_writable($this->logfile)) { + throw new Exception(l('login.log.error.permissions')); + } + + } + + /** + * Run an attempt to login + * + * @param string $username + * @param string $password + */ + public function attempt($username, $password) { + + $this->username = str::lower($username); + $this->password = $password; + + try { + + if($this->isInvalidUsername() || $this->isInvalidPassword()) { + throw new Exception(l('login.error')); + } + + $user = $this->user(); + + if(!$user->login($this->password)) { + throw new Exception(l('login.error')); + } + + $this->clearLog($this->visitorId()); + return true; + + } catch(Exception $e) { + + $this->log(); + $this->pause(); + + throw $e; + + } + + } + + /** + * Checks if the login form can be + * bypassed, because the user is already + * authenticated + * + * @return boolean + */ + public function isAuthenticated() { + try { + panel()->user(); + return true; + } catch(Exception $e) { + return false; + } + } + + /** + * Checks if a brute force attack has + * probably been executed + * + * @return boolean + */ + public function isBlocked() { + return $this->attempts() > $this->maxUntrustedAttempts; + } + + /** + * Fetch the user for the entered username + * + * @return User + */ + protected function user() { + return panel()->user($this->username); + } + + /** + * Returns all logdata in an array + * + * @return array + */ + protected function logdata() { + if(!is_null($this->logdata)) { + return $this->logdata; + } else { + + $data = (array)data::read($this->logfile, 'json'); + $login = $this; + + // remove old entries + $data = array_filter($data, function($entry) use($login) { + return ($entry['time'] > (time() - $login->logexpiry)); + }); + + return $this->logdata = $data; + } + } + + /** + * Stores a new login attempt to + * make it trackable later + * + * The store contains a sha1 hash of the ip + * + * @return boolean + */ + protected function log() { + + // get the latest logdata + $data = $this->logdata(); + + // store a new attempt + $data[] = array( + 'time' => time(), + 'id' => $this->visitorId(), + ); + + // write it to the logfile + return data::write($this->logfile, $data, 'json'); + + } + + /** + * Return a hashed version of the visitor ip + * + * @return string + */ + protected function visitorId() { + return sha1(visitor::ip()); + } + + /** + * Returns the number of attempts for + * the current visitor + * + * @return int + */ + protected function attempts() { + + $data = $this->logdata(); + $login = $this; + $data = array_filter($data, function($entry) use($login) { + return $login->visitorId() === $entry['id']; + }); + + return count($data); + + } + + /** + * Checks if an invalid username has been entered + * + * @return boolean + */ + protected function isInvalidUsername() { + return !preg_match('!^[a-z0-9._-]{1,}$!', $this->username); + } + + /** + * Checks if an invalid password has been entered + * + * @return boolean + */ + protected function isInvalidPassword() { + return empty($this->password); + } + + /** + * Create a random pause between 0 and 3 + * seconds to make it harder for attackers + * to execute many sequent attacks + */ + protected function pause() { + sleep(rand(1, 3)); + } + + /** + * Delete log entries by visitor id + */ + protected function clearLog($id) { + + $data = array_filter($this->logdata(), function($entry) use($id) { + return $entry['id'] !== $id; + }); + + data::write($this->logfile, $data, 'json'); + + // reset the logdata cache + $this->logdata = null; + + return $this->logdata(); + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/file.php b/panel/app/src/panel/models/file.php new file mode 100644 index 0000000..0974be6 --- /dev/null +++ b/panel/app/src/panel/models/file.php @@ -0,0 +1,250 @@ +page()->uri('file') . '/' . $this->encodedFilename() . '/' . $action; + } + } + + public function encodedFilename() { + if(php_sapi_name() == 'cli-server') { + $filename = str_replace('.', '․', $this->filename()); + } else { + $filename = $this->filename(); + } + return rawurlencode($filename); + } + + public static function decodeFilename($filename) { + $filename = rawurldecode($filename); + if(php_sapi_name() == 'cli-server') { + $filename = str_replace('․', '.', $filename); + } + return $filename; + } + + public function url($action = null) { + if(empty($action)) { + return parent::url(); + } else if($action == 'preview') { + return parent::url() . '?' . $this->modified(); + } else { + return panel()->urls()->index() . '/' . $this->uri($action); + } + } + + public function menu() { + return new Menu($this); + } + + public function form($action, $callback) { + return panel()->form('files/' . $action, $this, $callback); + } + + public function filterInput($input) { + return $input; + } + + public function getBlueprintFields() { + return $this->blueprint()->files()->fields($this); + } + + public function getFormFields() { + return $this->getBlueprintFields()->toArray(); + } + + public function getFormData() { + return $this->meta()->toArray(); + } + + public function canHavePreview() { + return $this->isWebImage() or $this->extension() == 'svg'; + } + + public function isWebImage() { + $images = array('image/jpeg', 'image/gif', 'image/png'); + return in_array($this->mime(), $images); + } + + public function canHaveThumb() { + if(!$this->isWebImage()) { + return false; + } else if(kirby()->option('thumbs.driver') == 'gd') { + if($this->width() > 2048 or $this->height() > 2048) { + return false; + } else { + return true; + } + } else { + return true; + } + } + + public function rename($name, $safeName = true) { + + // keep the old state of the file object + $old = clone $this; + + if($name == $this->name()) return true; + + // check if the name should be sanitized + $safeName = $this->page()->blueprint()->files()->sanitize(); + + // rename and get the new filename + $filename = parent::rename($name, $safeName); + + // clean the thumbs folder + $this->page()->removeThumbs(); + + // trigger the rename hook + kirby()->trigger('panel.file.rename', array($this, $old)); + + } + + public function update($data = array(), $sort = null, $trigger = true) { + + if($data == 'sort') { + parent::update(array('sort' => $sort)); + kirby()->trigger('panel.file.sort', $this); + return true; + } + + // rename the file if necessary + if(!empty($data['_name'])) { + $filename = $this->rename($data['_name']); + } + + // remove the name url and info + unset($data['_name']); + unset($data['_info']); + unset($data['_link']); + + if(!empty($data)) { + parent::update($data); + } + + if($trigger) { + kirby()->trigger('panel.file.update', $this); + } + + } + + public function replace() { + new Uploader($this->page, $this); + } + + public function delete() { + + parent::delete(); + + // clean the thumbs folder + $this->page()->removeThumbs(); + + kirby()->trigger('panel.file.delete', $this); + + } + + public function icon($position = 'left') { + + switch($this->type()) { + case 'image': + return icon('file-image-o', $position); + break; + case 'document': + switch($this->extension()) { + case 'pdf': + return icon('file-pdf-o', $position); + break; + case 'doc': + case 'docx': + return icon('file-word-o', $position); + break; + case 'xls': + return icon('file-excel-o', $position); + break; + default: + return icon('file-text-o', $position); + break; + } + break; + case 'code': + return icon('file-code-o', $position); + break; + case 'audio': + return icon('file-audio-o', $position); + break; + case 'video': + return icon('file-video-o', $position); + break; + default: + return icon('file-archive-o', $position); + break; + } + + } + + public function dragText() { + if(kirby()->option('panel.kirbytext') === false) { + switch($this->type()) { + case 'image': + return '![' . $this->name() . '](' . parent::url() . ')'; + break; + default: + return '[' . $this->filename() . '](' . parent::url() . ')'; + break; + } + } else { + switch($this->type()) { + case 'image': + return '(image: ' . $this->filename() . ')'; + break; + default: + return '(file: ' . $this->filename() . ')'; + break; + } + } + } + + public function topbar($topbar) { + + $this->files()->topbar($topbar); + + $topbar->append($this->url('edit'), $this->filename()); + + } + + public function createMeta($triggerUpdateHook = true) { + + // save default meta + $meta = array(); + + foreach($this->page()->blueprint()->files()->fields($this) as $field) { + $meta[$field->name()] = $field->default(); + } + + $this->update($meta, null, $triggerUpdateHook); + + return $this; + + } + + public function blueprint() { + return $this->page->blueprint(); + } + + public function structure() { + return new Structure($this, 'file_' . $this->page()->id() . '_' . $this->filename() . '_' . $this->site()->lang()); + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/file/menu.php b/panel/app/src/panel/models/file/menu.php new file mode 100644 index 0000000..b6a6bf1 --- /dev/null +++ b/panel/app/src/panel/models/file/menu.php @@ -0,0 +1,69 @@ +page = $file->page(); + $this->file = $file; + } + + public function item($icon, $label, $attr = array()) { + + $a = new Brick('a', '', $attr); + $a->append(icon($icon, 'left')); + $a->append(l($label)); + + $li = new Brick('li'); + $li->append($a); + + return $li; + + } + + public function previewOption() { + return $this->item('play-circle-o', 'files.show.open', array( + 'href' => $this->file->url('preview'), + 'target' => '_blank' + )); + } + + public function editOption() { + return $this->item('pencil', 'files.index.edit', array( + 'href' => $this->file->url('edit'), + )); + } + + public function deleteOption() { + return $this->item('trash-o', 'files.show.delete', array( + 'href' => $this->file->url('delete'), + 'data-modal' => true, + )); + } + + public function html() { + + $list = new Brick('ul'); + $list->addClass('nav nav-list'); + $list->addClass('dropdown-list'); + + $list->append($this->previewOption()); + $list->append($this->editOption()); + $list->append($this->deleteOption()); + + return ''; + + } + + public function __toString() { + return (string)$this->html(); + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/page.php b/panel/app/src/panel/models/page.php new file mode 100644 index 0000000..1be4764 --- /dev/null +++ b/panel/app/src/panel/models/page.php @@ -0,0 +1,644 @@ +cache['blueprint'])) return $this->cache['blueprint']; + + $blueprint = $this->intendedTemplate(); + + if(!Blueprint::exists($blueprint)) { + $blueprint = $this->template(); + } + + return $this->cache['blueprint'] = new Blueprint($blueprint); + + } + + public function createNum($to = null) { + + $parent = $this->parent(); + $params = $parent->blueprint()->pages()->num(); + + switch($params->mode()) { + case 'zero': + return 0; + break; + case 'date': + if($to = $this->date($params->format(), $params->field())) { + return $to; + } else { + return date($params->format()); + } + break; + default: + + $visibleSiblings = $parent->children()->visible(); + + if($to == 'last') { + $to = $visibleSiblings->count() + 1; + } else if($to == 'first') { + $to = 1; + } else if(is_null($to)) { + $to = $this->num(); + } + + if(!v::num($to)) return false; + + if($to <= 0) return 1; + + if($this->isInvisible()) { + $limit = $visibleSiblings->count() + 1; + } else { + $limit = $visibleSiblings->count(); + } + + if($limit < $to) { + $to = $limit; + } + + return intval($to); + break; + } + + } + + public function uri($action = null) { + if(empty($action)) { + return parent::uri(); + } else { + return 'pages/' . $this->id() . '/' . $action; + } + } + + public function url($action = null) { + if(empty($action)) { + return parent::url(); + } else if($action == 'preview') { + if($previewSetting = $this->blueprint()->preview()) { + switch($previewSetting) { + case 'parent': + return $this->parent() ? $this->parent()->url() : $this->url(); + break; + case 'first-child': + return $this->children()->first() ? $this->children()->first()->url() : false; + break; + case 'last-child': + return $this->children()->last() ? $this->children()->last()->url() : false; + break; + default: + return $this->url(); + break; + } + } else { + return false; + } + } else if($this->site->multilang() and $lang = $this->site->language($action)) { + return parent::url($lang->code()); + } else { + return panel()->urls()->index() . '/' . $this->uri($action); + } + } + + public function form($action, $callback) { + return panel()->form('pages/' . $action, $this, $callback); + } + + public function structure() { + return new Structure($this, 'page_' . $this->id() . '_' . $this->site->lang()); + } + + public function getFormData() { + + // get the latest content from the text file + $data = $this->content()->toArray(); + + // make sure the title is always there + $data['title'] = $this->title(); + + // add the changes to the content array + $data = array_merge($data, $this->changes()->get()); + + return $data; + + } + + public function getBlueprintFields() { + return $this->blueprint()->fields($this); + } + + public function getFormFields() { + + $fields = $this->getBlueprintFields()->toArray(); + + // add the title as hidden field + if(!isset($fields['title'])) { + $fields['title'] = array( + 'type' => 'hidden', + 'name' => 'title' + ); + } else { + // make sure the title field always has the type title + $fields['title']['type'] = 'title'; + } + + return $fields; + + } + + public function children() { + return new Children($this); + } + + public function canSortFiles() { + return $this->blueprint()->files()->sortable(); + } + + public function files() { + return new Files($this); + } + + public function addButton() { + try { + return new AddButton($this); + } catch(Exception $e) { + return false; + } + } + + public function menu($position = 'sidebar') { + return new Menu($this, $position); + } + + public function filterInput($input) { + return $input; + } + + public function changes() { + return new Changes($this); + } + + public function maxSubpages() { + $max = $this->blueprint()->pages()->max(); + // if max subpages is null, use the biggest 32bit integer + // will never be reached anyway. Kirby is not made for that scale :) + return is_null($max) ? 2147483647 : $max; + } + + public function maxFiles() { + $max = $this->blueprint()->files()->max(); + // see: maxSubpages + return is_null($max) ? 2147483647 : $max; + } + + public function canHaveSubpages() { + return $this->maxSubpages() !== 0; + } + + public function canShowSubpages() { + return ($this->blueprint()->pages()->hide() !== true and $this->canHaveSubpages()); + } + + public function canHaveFiles() { + return $this->maxFiles() !== 0; + } + + public function canShowFiles() { + return ($this->blueprint()->files()->hide() !== true and $this->canHaveFiles()); + } + + public function canHaveMoreSubpages() { + if(!$this->canHaveSubpages()) { + return false; + } else if($this->children()->count() >= $this->maxSubpages()) { + return false; + } else { + return true; + } + } + + public function canHaveMoreFiles() { + if(!$this->canHaveFiles()) { + return false; + } else if($this->files()->count() >= $this->maxFiles()) { + return false; + } else { + return true; + } + } + + public function canShowPreview() { + return $this->blueprint()->options()->preview(); + } + + public function canChangeStatus() { + return (!$this->isErrorPage() and $this->blueprint()->options()->status()) ? true : false; + } + + public function canChangeUrl() { + if($this->isHomePage() or $this->isErrorPage() or $this->blueprint()->options()->url() === false) { + return false; + } else { + return true; + } + } + + public function canChangeTemplate() { + if($this->isHomePage() or $this->isErrorPage() or $this->blueprint()->options()->template() === false) { + return false; + } else { + return $this->parent()->blueprint()->pages()->template()->count() > 1; + } + } + + public function move($uid) { + + $old = clone($this); + + if(!$this->canChangeUrl()) { + throw new Exception(l('pages.url.error.rights')); + } + + $site = panel()->site(); + $changes = $this->changes()->get(); + + $this->changes()->discard(); + + if($site->multilang() and $site->language()->code() != $site->defaultLanguage()->code()) { + parent::update(array( + 'URL-Key' => $uid + )); + } else { + parent::move($uid); + } + + $this->changes()->update($changes); + + // remove all thumbs for the old id + $old->removeThumbs(); + + // hit the hook + kirby()->trigger('panel.page.move', array($this, $old)); + + } + + public function _sort($to) { + if(is_dir($this->root())) { + return parent::sort($to); + } else { + return false; + } + } + + public function sort($to = null) { + + if($this->isErrorPage()) { + return $this->num(); + } + + // don't sort pages without permission to change the status + if($this->isInvisible() && !$this->canChangeStatus()) { + return false; + } + + // store the old number + $oldNum = $this->num(); + + // run the sorter + $this->sorter()->to($to); + + // run the hook if the number changed + if($oldNum != $this->num()) { + // hit the hook + kirby()->trigger('panel.page.sort', $this); + } + + return $this->num(); + + } + + public function sorter() { + return new Sorter($this); + } + + public function hide() { + + // don't hide pages, which are not allowed to change their status + if(!$this->canChangeStatus()) { + return false; + } + + parent::hide(); + $this->sorter()->hide(); + kirby()->trigger('panel.page.hide', $this); + + } + + public function toggle($position) { + + $mode = $this->parent()->blueprint()->pages()->num()->mode(); + $position = intval($position); + + if(($mode == 'default' && $position > 0) || !$this->isVisible()) { + $this->sort($position); + } else { + $this->hide(); + } + + } + + public function hasNoTitleField() { + $fields = $this->getFormFields(); + return empty($fields['title']); + } + + public function isHidden() { + return $this->blueprint()->hide() === true; + } + + public function isDeletable($exception = false) { + + if($this->isHomePage()) { + $error = 'pages.delete.error.home'; + } else if($this->isErrorPage()) { + $error = 'pages.delete.error.error'; + } else if($this->hasChildren()) { + $error = 'pages.delete.error.children'; + } else if(!$this->blueprint()->deletable() or !$this->blueprint()->options()->delete()) { + $error = 'pages.delete.error.blocked'; + } else { + return true; + } + + if($exception) { + throw new Exception($error); + } else { + return false; + } + + } + + public function sidebar() { + return new Sidebar($this); + } + + public function addToHistory() { + panel()->user()->history()->add($this); + } + + public function updateNum() { + + // make sure that the sorting number is correct + if($this->isVisible()) { + $this->sort($this->num()); + } + + return $this->num(); + + } + + public function updateUid() { + + // auto-update the uid if the sorting mode is set to zero + if($this->parent()->blueprint()->pages()->num()->mode() == 'zero') { + $uid = str::slug($this->title()); + $this->move($uid); + } + return $this->uid(); + + } + + public function update($data = array(), $lang = null) { + + $this->changes()->discard(); + + parent::update($data, $lang); + + // update the number if the date field + // changed for example + $this->updateNum(); + + kirby()->trigger('panel.page.update', $this); + + // add the page to the history + $this->addToHistory(); + + } + + public function upload() { + new Uploader($this); + } + + public function delete($force = false) { + + // delete the page + parent::delete(); + + // resort the siblings + $this->sorter()->delete(); + + // remove unsaved changes + $this->changes()->discard(); + + // delete all associated thumbs + $this->removeThumbs(); + + // hit the hook + kirby()->trigger('panel.page.delete', $this); + + } + + public function icon($position = 'left') { + return icon($this->blueprint()->icon(), $position); + } + + public function dragText() { + if(c::get('panel.kirbytext') === false) { + return '[' . $this->title() . '](' . $this->url() . ')'; + } else { + return '(link: ' . $this->uri() . ' text: ' . $this->title() . ')'; + } + } + + public function displayNum() { + + if($this->isInvisible()) { + return '—'; + } else { + + $numberSettings = $this->parent()->blueprint()->pages()->num(); + + switch($numberSettings->mode()) { + case 'zero': + if($numberSettings->display()) { + // customer number display + return $this->{$numberSettings->display()}(); + } else { + // alphabetic display numbers + return str::substr($this->title(), 0, 1); + } + break; + case 'date': + return $this->date($numberSettings->display(), $numberSettings->field()); + break; + default: + if($numberSettings->display()) { + // customer number display + return $this->{$numberSettings->display()}(); + } else { + // regular number display + return intval($this->num()); + } + break; + } + + } + + } + + public function topbar(Topbar $topbar) { + + foreach($this->parents()->flip() as $item) { + $topbar->append($item->url('edit'), $item->title()); + } + + $topbar->append($this->url('edit'), $this->title()); + + if($topbar->view == 'subpages/index') { + $topbar->append($this->url('subpages'), l('subpages')); + } + + $topbar->html .= new Snippet('languages', array( + 'languages' => $this->site()->languages(), + 'language' => $this->site()->language(), + )); + + } + + public function changeTemplate($newTemplate) { + + $oldTemplate = $this->intendedTemplate(); + + if($newTemplate == $oldTemplate) return true; + + if($this->site()->multilang()) { + + foreach($this->site()->languages() as $lang) { + $old = $this->textfile(null, $lang->code()); + $new = $this->textfile($newTemplate, $lang->code()); + f::move($old, $new); + $this->reset(); + $this->updateForNewTemplate($oldTemplate, $newTemplate, $lang->code()); + } + + } else { + $old = $this->textfile(); + $new = $this->textfile($newTemplate); + f::move($old, $new); + $this->reset(); + $this->updateForNewTemplate($oldTemplate, $newTemplate); + } + + return true; + + } + + public function prepareForNewTemplate($oldTemplate, $newTemplate, $language = null) { + + $data = array(); + $incompatible = array(); + $content = $this->content($language); + $oldBlueprint = new Blueprint($oldTemplate); + $oldFields = $oldBlueprint->fields($this); + $newBlueprint = new Blueprint($newTemplate); + $newFields = $newBlueprint->fields($this); + + // log + $removed = array(); + $replaced = array(); + $added = array(); + + // first overwrite everything + foreach($oldFields as $oldField) { + $data[$oldField->name()] = null; + } + + // now go through all new fileds and compare them to the old field types + foreach($newFields as $newField) { + + $oldField = $oldFields->{$newField->name()}; + + // only take data from fields with matching names and types + if($oldField and $oldField->type() == $newField->type()) { + $data[$newField->name()] = $content->get($newField->name())->value(); + } else { + $data[$newField->name()] = $newField->default(); + + if($oldField) { + $replaced[$newField->name()] = $newField->label(); + } else { + $added[$newField->name()] = $newField->label(); + } + + } + + } + + foreach($data as $name => $content) { + if(is_null($content)) $removed[$name] = $oldFields->{$name}->label(); + } + + return array( + 'data' => $data, + 'removed' => $removed, + 'replaced' => $replaced, + 'added' => $added + ); + + } + + public function updateForNewTemplate($oldTemplate, $newTemplate, $language = null) { + $prep = $this->prepareForNewTemplate($oldTemplate, $newTemplate, $language); + $this->update($prep['data'], $language); + } + + /** + * Clean the thumbs folder for the page + * + */ + public function removeThumbs() { + return dir::remove($this->kirby()->roots()->thumbs() . DS . $this->id()); + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/page/addbutton.php b/panel/app/src/panel/models/page/addbutton.php new file mode 100644 index 0000000..e66b1e8 --- /dev/null +++ b/panel/app/src/panel/models/page/addbutton.php @@ -0,0 +1,21 @@ +page = $page; + $this->modal = true; + $this->url = $this->page->url('add'); + + if(!$this->page->canHaveMoreSubpages()) { + throw new Exception(l('subpages.add.error.more')); + } + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/page/blueprint.php b/panel/app/src/panel/models/page/blueprint.php new file mode 100644 index 0000000..e988d37 --- /dev/null +++ b/panel/app/src/panel/models/page/blueprint.php @@ -0,0 +1,126 @@ +load($name); + + $this->title = a::get($this->yaml, 'title', 'Page'); + $this->preview = a::get($this->yaml, 'preview', 'page'); + $this->deletable = a::get($this->yaml, 'deletable', true); + $this->icon = a::get($this->yaml, 'icon', 'file-o'); + $this->hide = a::get($this->yaml, 'hide', false); + $this->type = a::get($this->yaml, 'type', 'page'); + $this->pages = new Pages(a::get($this->yaml, 'pages', true)); + $this->files = new Files(a::get($this->yaml, 'files', true)); + $this->options = new Options(a::get($this->yaml, 'options', array())); + + } + + public function load($name) { + + // make sure there's no path included in the name + $name = basename(strtolower($name)); + + if(isset(static::$cache[$name])) { + $this->file = static::$cache[$name]['file']; + $this->name = static::$cache[$name]['name']; + $this->yaml = static::$cache[$name]['yaml']; + return true; + } + + // find the matching blueprint file + $file = kirby()->get('blueprint', $name); + + if($file) { + + $this->file = $file; + $this->name = $name; + $this->yaml = data::read($this->file, 'yaml'); + + // remove the broken first line + unset($this->yaml[0]); + + static::$cache[$name] = array( + 'file' => $this->file, + 'name' => $this->name, + 'yaml' => $this->yaml + ); + + return true; + + } else if($name == 'default') { + throw new Exception(l('blueprints.error.default.missing')); + } else { + return $this->load('default'); + } + + } + + public function fields($model) { + $fields = a::get($this->yaml, 'fields', array()); + return new Fields($fields, $model); + } + + static public function exists($name) { + return kirby()->get('blueprint', $name) ? true : false; + } + + static public function all() { + + $files = dir::read(static::$root); + $result = array_keys(kirby()->get('blueprint')); + $home = kirby()->option('home', 'home'); + $error = kirby()->option('error', 'error'); + + foreach($files as $file) { + + $name = f::name($file); + + if($name != 'site' and $name != $home and $name != $error) { + $result[] = $name; + } + + } + + return $result; + + } + + public function __toString() { + return $this->name; + } + +} diff --git a/panel/app/src/panel/models/page/blueprint/field.php b/panel/app/src/panel/models/page/blueprint/field.php new file mode 100644 index 0000000..6bbd828 --- /dev/null +++ b/panel/app/src/panel/models/page/blueprint/field.php @@ -0,0 +1,115 @@ +_extend($params); + } + + if(a::get($params, 'name') == 'title') { + $params['type'] = 'title'; + + if(!isset($params['required'])) { + $params['required'] = true; + } + } + + if(empty($params['type'])) { + $params['type'] = 'text'; + } + + // lowercase the type + $params['type'] = strtolower($params['type']); + + // register the parent model + $params['model'] = $model; + + // try to fetch the parent page from the model + if(is_a($model, 'Page')) { + $params['page'] = $model; + } else if(is_a($model, 'File')) { + $params['page'] = $model->page(); + } + + // create the default value + $params['default'] = $this->_default(a::get($params, 'default')); + + parent::__construct($params); + + } + + + public function _extend($params) { + + $extends = $params['extends']; + $snippet = f::resolve(kirby()->roots()->blueprints() . DS . 'fields' . DS . $extends, array('yml', 'php', 'yaml')); + + if(empty($snippet)) { + throw new Exception(l('fields.error.extended')); + } + + $yaml = data::read($snippet, 'yaml'); + $params = a::merge($yaml, $params); + + return $params; + + } + + public function _default($default) { + + if($default === true) { + return 'true'; + } else if($default === false) { + return 'false'; + } else if(empty($default) and strlen($default) == 0) { + return ''; + } else if(is_string($default)) { + return $default; + } else { + $type = a::get($default, 'type'); + + switch($type) { + case 'date': + $format = a::get($default, 'format', 'Y-m-d'); + return date($format); + break; + case 'datetime': + $format = a::get($default, 'format', 'Y-m-d H:i:s'); + return date($format); + break; + case 'user': + $user = isset($default['user']) ? site()->users()->find($default['user']) : site()->user(); + if(!$user) return ''; + return (isset($default['field']) and $default['field'] != 'password') ? $user->{$default['field']}() : $user->username(); + break; + case 'structure': + return "\n" . \data::encode(array($default), 'yaml') . "\n"; + break; + default: + return $default; + break; + } + + } + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/page/blueprint/fields.php b/panel/app/src/panel/models/page/blueprint/fields.php new file mode 100644 index 0000000..e334a0a --- /dev/null +++ b/panel/app/src/panel/models/page/blueprint/fields.php @@ -0,0 +1,48 @@ + $field) { + + // sanitize the name + $name = str_replace('-','_', str::lower($name)); + + // import a field by name + if(is_string($field)) { + $field = array( + 'name' => $name, + 'extends' => $field + ); + } + + // add the name to the field + $field['name'] = $name; + + // create the field object + $field = new Field($field, $model); + + // append it to the collection + $this->append($name, $field); + + } + + } + + public function toArray($callback = null) { + $result = array(); + foreach($this->data as $field) { + $result[$field->name()] = $field->toArray(); + } + return $result; + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/page/blueprint/files.php b/panel/app/src/panel/models/page/blueprint/files.php new file mode 100644 index 0000000..02385a9 --- /dev/null +++ b/panel/app/src/panel/models/page/blueprint/files.php @@ -0,0 +1,57 @@ +params = $params; + + if($params === false) { + $this->fields = array(); + $this->type = array(); + $this->size = false; + $this->width = false; + $this->height = false; + $this->max = 0; + $this->hide = true; + $this->sortable = false; + + } else if(is_array($params)) { + $this->fields = a::get($params, 'fields', $this->fields); + $this->type = a::get($params, 'type', $this->type); + if (!is_array($this->type)) + $this->type = array($this->type); + $this->size = a::get($params, 'size', $this->size); + $this->width = a::get($params, 'width', $this->width); + $this->height = a::get($params, 'height', $this->height); + $this->max = a::get($params, 'max', $this->max); + $this->hide = a::get($params, 'hide', $this->hide); + $this->sort = a::get($params, 'sort', $this->sort); + $this->sortable = a::get($params, 'sortable', $this->sortable); + $this->sanitize = a::get($params, 'sanitize', true); + } + + } + + public function fields($file) { + return new Fields($this->fields, $file); + } + +} diff --git a/panel/app/src/panel/models/page/blueprint/options.php b/panel/app/src/panel/models/page/blueprint/options.php new file mode 100644 index 0000000..305c96e --- /dev/null +++ b/panel/app/src/panel/models/page/blueprint/options.php @@ -0,0 +1,46 @@ +preview = a::get($options, 'preview', true); + $this->status = a::get($options, 'status', true); + $this->template = a::get($options, 'template', true); + $this->url = a::get($options, 'url', true); + $this->delete = a::get($options, 'delete', true); + } + + public function preview() { + return $this->preview; + } + + public function status() { + return $this->status; + } + + public function template() { + return $this->template; + } + + public function url() { + return $this->url; + } + + public function delete() { + return $this->delete; + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/page/blueprint/pages.php b/panel/app/src/panel/models/page/blueprint/pages.php new file mode 100644 index 0000000..9540c0e --- /dev/null +++ b/panel/app/src/panel/models/page/blueprint/pages.php @@ -0,0 +1,96 @@ +template = blueprint::all(); + } else if($params === false) { + $this->limit = 0; + $this->max = 0; + $this->sortable = false; + $this->hide = true; + } else if(is_array($params)) { + $template = a::get($params, 'template'); + if($template == false) { + $this->template = blueprint::all(); + } else if(is_array($template)) { + $this->template = $template; + } else { + $this->template = array($template); + } + $this->sort = a::get($params, 'sort', $this->sort); + $this->sortable = a::get($params, 'sortable', $this->sortable); + $this->limit = a::get($params, 'limit', $this->limit); + $this->num = a::get($params, 'num', $this->num); + $this->max = a::get($params, 'max', $this->max); + $this->hide = a::get($params, 'hide', $this->hide); + $this->build = a::get($params, 'build', $this->build); + } else if(is_string($params)) { + $this->template = array($params); + } + + } + + public function template() { + $result = array(); + foreach($this->template as $t) { + $result[$t] = new Blueprint($t); + } + return new Collection($result); + } + + public function num() { + + $obj = new Obj(); + + $obj->mode = 'default'; + $obj->field = null; + $obj->format = null; + $obj->display = null; + + if(is_array($this->num)) { + foreach($this->num as $k => $v) $obj->$k = $v; + } else if(!empty($this->num)) { + $obj->mode = $this->num; + } + + switch($obj->mode) { + case 'field': + isset($obj->field) or $obj->field = 'num'; + break; + case 'date': + // switch the default date format by configured handler + $defaultDateFormat = kirby()->option('date.handler') == 'strftime' ? '%Y%m%d' : 'Ymd'; + $defaultDisplayFormat = kirby()->option('date.handler') == 'strftime' ? '%Y/%m/%d' : 'Y/m/d'; + + // set the defaults + isset($obj->field) or $obj->field = 'date'; + isset($obj->format) or $obj->format = $defaultDateFormat; + isset($obj->display) or $obj->display = $defaultDisplayFormat; + break; + } + + return $obj; + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/page/changes.php b/panel/app/src/panel/models/page/changes.php new file mode 100644 index 0000000..bae4bd2 --- /dev/null +++ b/panel/app/src/panel/models/page/changes.php @@ -0,0 +1,121 @@ +model = $model; + } + + public function data() { + return s::get('changes', array()); + } + + public function id() { + $site = panel()->site(); + if($site->multilang()) { + return $site->language()->code() . '-' . sha1($this->model->id()); + } else { + return sha1($this->model->id()); + } + } + + public function keep() { + + $blueprint = $this->model->blueprint(); + $fields = $blueprint->fields($this->model); + $form = new Form($fields->toArray()); + $data = $this->model->filterInput($form->serialize()); + $old = $this->model->content()->toArray(); + + if($data != $old) { + $this->update($data); + } + + } + + public function discard($field = null) { + + $store = $this->data(); + + if(is_null($field)) { + unset($store[$this->id()]); + } else { + unset($store[$this->id()][$field]); + } + + s::set('changes', $store); + + // remove all structures from the session as well + $this->model->structure()->reset(); + + return $store; + + } + + public function differ() { + + $data = $this->get(); + $changes = false; + + foreach($data as $field => $value) { + + $object = $this->model->{$field}(); + + if(!method_exists($object, '__toString')) { + continue; + } + + if((string)$object !== $value) { + $changes = true; + } + + } + + return $changes; + + } + + public function get($field = null) { + + $data = (array)a::get($this->data(), $this->id()); + + if(!is_null($field)) { + return a::get($data, $field); + } else { + return $data; + } + + } + + public function update($field, $data = null) { + + if(is_null($data) and is_array($field)) { + $store = $this->data(); + $store[$this->id()] = $field; + } else if(is_string($field)) { + $store = $this->data(); + if(!isset($store[$this->id()]) or !is_array($store[$this->id()])) { + $store[$this->id()] = array(); + } + $store[$this->id()][$field] = $data; + } + + s::set('changes', $store); + return $store; + + } + + public function flush() { + s::set('changes', array()); + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/page/menu.php b/panel/app/src/panel/models/page/menu.php new file mode 100644 index 0000000..692d523 --- /dev/null +++ b/panel/app/src/panel/models/page/menu.php @@ -0,0 +1,166 @@ +page = $page; + $this->parent = $page->parent(); + $this->blueprint = $page->blueprint(); + $this->position = $position; + } + + public function item($icon, $label, $attr = array()) { + + $a = new Brick('a', '', $attr); + $a->append(icon($icon, 'left')); + $a->append(l($label) ?: $label); + + $li = new Brick('li'); + $li->append($a); + + return $li; + + } + + public function modalUrl($action) { + + if($this->position == 'context') { + if($this->parent->isSite()) { + $redirect = '/'; + } else { + $redirect = $this->parent->uri('edit'); + } + return $this->page->url($action) . '?_redirect=' . $redirect; + } else { + return $this->page->url($action); + } + + } + + public function previewOption() { + if($preview = $this->page->url('preview') and $this->page->canShowPreview()) { + return $this->item('play-circle-o', 'pages.show.preview', array( + 'href' => $preview, + 'target' => '_blank', + 'title' => 'p', + 'data-shortcut' => 'p', + )); + } else { + return false; + } + } + + public function editOption() { + if($this->position == 'context') { + return $this->item('pencil', 'pages.show.subpages.edit', array( + 'href' => $this->page->url('edit'), + )); + } + } + + public function statusOption() { + + if($this->page->canChangeStatus()) { + + if($this->page->isInvisible()) { + $icon = 'toggle-off'; + $label = 'pages.show.invisible'; + } else { + $icon = 'toggle-on'; + $label = 'pages.show.visible'; + } + + return $this->item($icon, $label, array( + 'href' => $this->modalUrl('toggle'), + 'data-modal' => true, + )); + + } else { + return false; + } + + } + + public function templateOption() { + if($this->page->canChangeTemplate()) { + return $this->item('file-code-o', l('pages.show.template') . ': ' . i18n($this->page->blueprint()->title()), array( + 'href' => $this->modalUrl('template'), + 'data-modal' => true, + 'data-shortcut' => 't', + )); + } else { + return false; + } + } + + public function urlOption() { + if($this->page->canChangeUrl()) { + return $this->item('chain', 'pages.show.changeurl', array( + 'href' => $this->modalUrl('url'), + 'title' => 'u', + 'data-shortcut' => 'u', + 'data-modal' => true, + )); + } else { + return false; + } + } + + public function deleteOption() { + if($this->page->isDeletable()) { + return $this->item('trash-o', 'pages.show.delete', array( + 'href' => $this->modalUrl('delete'), + 'title' => '#', + 'data-shortcut' => '#', + 'data-modal' => true, + )); + } else { + return false; + } + } + + public function html() { + + $list = new Brick('ul'); + $list->addClass('nav nav-list'); + + if($this->position == 'sidebar') { + $list->addClass('sidebar-list'); + } else { + $list->addClass('dropdown-list'); + } + + $list->append($this->previewOption()); + $list->append($this->editOption()); + $list->append($this->statusOption()); + $list->append($this->templateOption()); + $list->append($this->urlOption()); + $list->append($this->deleteOption()); + + if($this->position == 'context') { + return ''; + } else { + return $list; + } + + } + + public function __toString() { + try { + return (string)$this->html(); + } catch(Exception $e) { + return (string)$e->getMessage(); + } + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/page/sidebar.php b/panel/app/src/panel/models/page/sidebar.php new file mode 100644 index 0000000..4e6dee2 --- /dev/null +++ b/panel/app/src/panel/models/page/sidebar.php @@ -0,0 +1,78 @@ +page = $page; + $this->blueprint = $page->blueprint(); + } + + public function subpages() { + + if(!$this->page->canShowSubpages()) { + return null; + } + + // fetch all subpages in the right order + $children = $this->page->children()->paginated('sidebar'); + + // create the pagination snippet + $pagination = new Snippet('pagination', array( + 'pagination' => $children->pagination(), + 'nextUrl' => $children->pagination()->nextPageUrl(), + 'prevUrl' => $children->pagination()->prevPageUrl(), + )); + + // create the snippet and fill it with all data + return new Snippet('pages/sidebar/subpages', array( + 'title' => l('pages.show.subpages.title'), + 'page' => $this->page, + 'subpages' => $children, + 'addbutton' => $this->page->addButton(), + 'pagination' => $pagination, + )); + + } + + public function files() { + + if(!$this->page->canShowFiles()) { + return null; + } + + return new Snippet('pages/sidebar/files', array( + 'page' => $this->page, + 'files' => $this->page->files(), + )); + + } + + public function render() { + + // create the monster sidebar + return new Snippet('pages/sidebar', array( + 'page' => $this->page, + 'menu' => $this->page->menu('sidebar'), + 'subpages' => $this->subpages(), + 'files' => $this->files(), + )); + + } + + public function __toString() { + try { + return (string)$this->render(); + } catch(Exception $e) { + return $e->getMessage(); + } + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/page/sorter.php b/panel/app/src/panel/models/page/sorter.php new file mode 100644 index 0000000..a2aa0f0 --- /dev/null +++ b/panel/app/src/panel/models/page/sorter.php @@ -0,0 +1,123 @@ +page = $page; + $this->parent = $page->parent(); + $this->params = $this->parent->blueprint()->pages()->num(); + $this->siblings = $this->parent->children()->visible(); + + } + + protected function execute() { + + switch($this->params->mode()) { + case 'date': + $this->date(); + break; + case 'zero': + $this->zero(); + break; + default: + $this->num(); + break; + } + + } + + protected function zero() { + foreach($this->siblings as $sibling) { + $sibling->_sort(0); + } + } + + protected function date() { + + foreach($this->siblings as $sibling) { + + // get the date + $date = $sibling->date($this->params->format(), $this->params->field()); + + // take the current date if the date is missing + if(!$date) { + $handler = kirby()->option('date.handler'); + $date = $handler($this->params->format()); + } + + $sibling->_sort($date); + + } + + } + + protected function num() { + + // make sure the siblings are sorted correctly + $this->siblings = $this->siblings->not($this->page)->sortBy('num', 'asc'); + + // special keywords and sanitization + if($this->to == 'last') { + $this->to = $this->siblings->count() + 1; + } else if($this->to == 'first') { + $this->to = 1; + } else if($this->to === false) { + $this->to = false; + } else if($this->to < 1) { + $this->to = 1; + } + + // start the index + $n = 0; + + if($this->to === false) { + foreach($this->siblings as $sibling) { + $n++; $sibling->_sort($n); + } + } else { + + // go through all items before the selected page + foreach($this->siblings->slice(0, $this->to - 1) as $sibling) { + $n++; $sibling->_sort($n); + } + + // add the selected page + $n++; $this->page->_sort($n); + + // go through all the items after the selected page + foreach($this->siblings->slice($this->to - 1) as $sibling) { + $n++; $sibling->_sort($n); + } + + } + + } + + public function to($to) { + $this->siblings->data[$this->page->id()] = $this->page; + $this->to = $to; + $this->execute(); + } + + public function delete() { + $this->siblings = $this->siblings->not($this->page); + $this->to = false; + $this->execute(); + } + + public function hide() { + $this->siblings = $this->siblings->not($this->page); + $this->to = false; + $this->execute(); + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/page/uploader.php b/panel/app/src/panel/models/page/uploader.php new file mode 100644 index 0000000..09da27d --- /dev/null +++ b/panel/app/src/panel/models/page/uploader.php @@ -0,0 +1,188 @@ +page = $page; + $this->file = $file; + $this->blueprint = $page->blueprint(); + $this->filename = $this->blueprint->files()->sanitize() ? '{safeFilename}' : '{filename}'; + + if($this->file) { + $this->replace(); + } else { + $this->upload(); + } + + } + + public function upload() { + + // check if more files can be uploaded for the page + if(!$this->page->canHaveMoreFiles()) { + throw new Exception(l('files.add.error.max')); + } + + $upload = new Upload($this->page->root() . DS . $this->filename, array( + 'overwrite' => true, + 'accept' => function($file) { + + $callback = kirby()->option('panel.upload.accept'); + + if(is_callable($callback)) { + return call($callback, $file); + } else { + return true; + } + + } + )); + + $file = $this->move($upload); + + // create the initial meta file + // without triggering the update hook + $file->createMeta(false); + + // make sure that the file is being marked as updated + touch($file->root()); + + // clean the thumbs folder + $this->page->removeThumbs(); + + kirby()->trigger('panel.file.upload', $file); + + } + + public function replace() { + + $file = $this->file; + $upload = new Upload($file->root(), array( + 'overwrite' => true, + 'accept' => function($upload) use($file) { + if($upload->mime() != $file->mime()) { + throw new Error(l('files.replace.error.type')); + } + } + )); + + $file = $this->move($upload); + + // make sure that the file is being marked as updated + touch($file->root()); + + // clean the thumbs folder + $this->page->removeThumbs(); + + kirby()->trigger('panel.file.replace', $file); + + } + + public function move($upload) { + + // flush all cached files + $this->page->reset(); + + // get the file object from the upload + $uploaded = $upload->file(); + + // check if the upload worked + if(!$uploaded) { + throw new Exception($upload->error()->getMessage()); + } + + // check if the page has such a file + $file = $this->page->file($uploaded->filename()); + + // delete the upload if something went wrong + if(!$file) { + $uploaded->delete(); + throw new Exception(l('files.error.missing.file')); + } + + try { + // security checks + $this->checkUpload($file); + return $file; + } catch(Exception $e) { + $file->delete(); + throw $e; + } + + } + + public function checkUpload($file) { + + $filesettings = $this->blueprint->files(); + $forbiddenExtensions = array('php', 'html', 'htm', 'exe', kirby()->option('content.file.extension', 'txt')); + $forbiddenMimes = array_merge(f::$mimes['php'], array('text/html', 'application/x-msdownload')); + $extension = strtolower($file->extension()); + + // files without extension are not allowed + if(empty($extension)) { + throw new Exception(l('files.add.error.extension.missing')); + } + + // block forbidden extensions + if(in_array($extension, $forbiddenExtensions)) { + throw new Exception(l('files.add.error.extension.forbidden')); + } + + // especially block any connection that contains php + if(str::contains($extension, 'php')) { + throw new Exception(l('files.add.error.extension.forbidden')); + } + + // block forbidden mimes + if(in_array(strtolower($file->mime()), $forbiddenMimes)) { + throw new Exception(l('files.add.error.mime.forbidden')); + } + + // Block htaccess files + if(strtolower($file->filename()) == '.htaccess') { + throw new Exception(l('files.add.error.htaccess')); + } + + // Block invisible files + if(str::startsWith($file->filename(), '.')) { + throw new Exception(l('files.add.error.invisible')); + } + + // Files blueprint option 'type' + if(count($filesettings->type()) > 0 and !in_array($file->type(), $filesettings->type())) { + throw new Exception(l('files.add.blueprint.type.error') . implode(', ', $filesettings->type())); + } + + // Files blueprint option 'size' + if($filesettings->size() and f::size($file->root()) > $filesettings->size()) { + throw new Exception(l('files.add.blueprint.size.error') . f::niceSize($filesettings->size())); + } + + // Files blueprint option 'width' + if($file->type() == 'image' and $filesettings->width() and $file->width() > $filesettings->width()) { + throw new Exception('Page only allows image width of ' . $filesettings->width().'px'); + } + + // Files blueprint option 'height' + if($file->type() == 'image' and $filesettings->height() and $file->height() > $filesettings->height()) { + throw new Exception('Page only allows image height of ' . $filesettings->height().'px'); + } + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/site.php b/panel/app/src/panel/models/site.php new file mode 100644 index 0000000..e6de5b2 --- /dev/null +++ b/panel/app/src/panel/models/site.php @@ -0,0 +1,222 @@ +cache['blueprint'])) return $this->cache['blueprint']; + return $this->cache['blueprint'] = new Blueprint('site'); + } + + public function changes() { + return new Changes($this); + } + + public function uri($action = null) { + if(empty($action)) { + return parent::uri(); + } else { + return 'site/' . $action; + } + } + + public function url($action = null) { + + if(empty($action)) { + return parent::url(); + } else if($action == 'edit') { + return panel()->urls()->index() . '/options'; + } else if($action == 'preview') { + return parent::url(); + } else if($this->multilang() and in_array($action, $this->languages()->codes())) { + return parent::url($action); + } else { + return panel()->urls()->index() . '/' . $this->uri($action); + } + + } + + public function form($action, $callback) { + return panel()->form('pages/' . $action, $this, $callback); + } + + public function getFormData() { + + // get the latest content from the text file + $data = $this->content()->toArray(); + + // make sure the title is always there + $data['title'] = $this->title(); + + return $data; + + } + + public function getBlueprintFields() { + return $this->blueprint()->fields($this); + } + + public function getFormFields() { + return $this->getBlueprintFields()->toArray(); + } + + public function canSortFiles() { + return $this->blueprint()->files()->sortable(); + } + + public function files() { + return new Files($this); + } + + public function children() { + return new Children($this); + } + + public function filterInput($input) { + $data = array(); + foreach($this->content()->toArray() as $key => $value) { + $data[$key] = null; + } + return array_merge($data, $input); + } + + public function update($input = array(), $lang = null) { + + $data = $this->filterInput($input); + + $this->changes()->discard(); + + parent::update($data, $lang); + + kirby()->trigger('panel.site.update', $this); + + } + + public function sidebar() { + return new Sidebar($this); + } + + public function upload() { + return new Uploader($this); + } + + public function addButton() { + try { + return new AddButton($this); + } catch(Exception $e) { + return false; + } + } + + public function topbar(Topbar $topbar) { + + if($topbar->view == 'options/index') { + $topbar->append(purl('options'), l('metatags')); + } + + if($topbar->view == 'subpages/index') { + $topbar->append($this->url('subpages'), l('subpages')); + } + + $topbar->html .= new Snippet('languages', array( + 'languages' => $this->languages(), + 'language' => $this->language(), + )); + + } + + public function users() { + return new Users(); + } + + public function user($username = null) { + if(is_null($username)) return User::current(); + try { + return new User($username); + } catch(Exception $e) { + return null; + } + } + + public function delete($force = false) { + throw new Exception(l('site.delete.error')); + } + + public function maxSubpages() { + $max = $this->blueprint()->pages()->max(); + // if max subpages is null, use the biggest 32bit integer + // will never be reached anyway. Kirby is not made for that scale :) + return is_null($max) ? 2147483647 : $max; + } + + public function maxFiles() { + $max = $this->blueprint()->files()->max(); + // see: maxSubpages + return is_null($max) ? 2147483647 : $max; + } + + public function canHaveSubpages() { + return $this->maxSubpages() !== 0; + } + + public function canShowSubpages() { + return ($this->blueprint()->pages()->hide() !== true and $this->canHaveSubpages()); + } + + public function canHaveFiles() { + return $this->maxFiles() !== 0; + } + + public function canShowFiles() { + return ($this->blueprint()->files()->hide() !== true and $this->canHaveFiles()); + } + + public function canHaveMoreSubpages() { + if(!$this->canHaveSubpages()) { + return false; + } else if($this->children()->count() >= $this->maxSubpages()) { + return false; + } else { + return true; + } + } + + public function canHaveMoreFiles() { + if(!$this->canHaveFiles()) { + return false; + } else if($this->files()->count() >= $this->maxFiles()) { + return false; + } else { + return true; + } + } + + + public function structure() { + return new Structure($this, 'site_' . $this->lang()); + } + + public function lang() { + return $this->multilang() ? $this->language()->code() : false; + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/user.php b/panel/app/src/panel/models/user.php new file mode 100644 index 0000000..b897c15 --- /dev/null +++ b/panel/app/src/panel/models/user.php @@ -0,0 +1,151 @@ +username() . '/' . $action; + } + + public function url($action = 'edit') { + if(empty($action)) $action = 'edit'; + return panel()->urls()->index() . '/' . $this->uri($action); + } + + public function form($action, $callback) { + return panel()->form('users/' . $action, $this, $callback); + } + + public function update($data = array()) { + + if(!panel()->user()->isAdmin() and !$this->isCurrent()) { + throw new Exception(l('users.form.error.update.rights')); + } + + // users which are not an admin cannot change their role + if(!panel()->user()->isAdmin()) { + unset($data['role']); + } + + if(str::length(a::get($data, 'password')) > 0) { + if(a::get($data, 'password') !== a::get($data, 'passwordconfirmation')) { + throw new Exception(l('users.form.error.password.confirm')); + } + } else { + unset($data['password']); + } + + unset($data['passwordconfirmation']); + + if($this->isLastAdmin() and a::get($data, 'role') !== 'admin') { + // check the number of left admins to not convert the last one + throw new Exception(l('user.error.lastadmin')); + } + + parent::update($data); + + // flush the cache in case if the user data is + // used somewhere on the site (i.e. for profiles) + kirby()->cache()->flush(); + + kirby()->trigger('panel.user.update', $this); + + return $this; + + } + + public function isLastAdmin() { + if($this->isAdmin()) { + if(panel()->users()->filterBy('role', 'admin')->count() == 1) { + return true; + } + } else { + return false; + } + } + + public function delete() { + + if(!panel()->user()->isAdmin() and !$this->isCurrent()) { + throw new Exception(l('users.delete.error.permission')); + } + + if($this->isLastAdmin()) { + // check the number of left admins to not delete the last one + throw new Exception(l('users.delete.error.lastadmin')); + } + + parent::delete(); + + // flush the cache in case if the user data is + // used somewhere on the site (i.e. for profiles) + kirby()->cache()->flush(); + + kirby()->trigger('panel.user.delete', $this); + + } + + public function avatar($crop = null) { + if($crop === null) { + return new Avatar($this); + } else { + $avatar = $this->avatar(); + if($avatar->exists()) { + return $avatar->crop($crop); + } else { + return $avatar; + } + } + } + + public function isCurrent() { + return $this->is(panel()->user()); + } + + public function history() { + return new History($this); + } + + public function topbar($topbar) { + + $topbar->append(purl('users'), l('users')); + $topbar->append($this->url(), $this->username()); + + } + + public function getBlueprintFields() { + return $this->blueprint()->fields($this); + } + + public function blueprint() { + return new Blueprint($this); + } + + public function structure() { + return new Structure($this, 'user_' . $this->username()); + } + + static public function current() { + if($user = parent::current()) { + if($user->hasPanelAccess()) { + return $user; + } else { + $user->logout(); + return null; + } + } else { + return null; + } + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/user/avatar.php b/panel/app/src/panel/models/user/avatar.php new file mode 100644 index 0000000..1a6e26d --- /dev/null +++ b/panel/app/src/panel/models/user/avatar.php @@ -0,0 +1,78 @@ +exists()) { + $this->root = $this->user->avatarRoot('{safeExtension}'); + $this->url = purl('assets/images/avatar.png'); + } + + } + + public function form($action, $callback) { + return panel()->form('avatars/' . $action, $this, $callback); + } + + public function upload() { + + if(!panel()->user()->isAdmin() and !$this->user->isCurrent()) { + throw new Exception(l('users.avatar.error.permission')); + } + + $root = $this->exists() ? $this->root() : $this->user->avatarRoot('{safeExtension}'); + + $upload = new Upload($root, array( + 'accept' => function($upload) { + if($upload->type() != 'image') { + throw new Error(l('users.avatar.error.type')); + } + } + )); + + if(!$upload->file()) { + throw $upload->error(); + } + + // flush the cache in case if the user data is + // used somewhere on the site (i.e. for profiles) + kirby()->cache()->flush(); + + kirby()->trigger('panel.avatar.upload', $this); + + } + + public function delete() { + + if(!panel()->user()->isAdmin() and !$this->user->isCurrent()) { + throw new Exception(l('users.avatar.delete.error.permission')); + } else if(!$this->exists()) { + return true; + } + + if(!parent::delete()) { + throw new Exception(l('users.avatar.delete.error')); + } + + // flush the cache in case if the user data is + // used somewhere on the site (i.e. for profiles) + kirby()->cache()->flush(); + + kirby()->trigger('panel.avatar.delete', $this); + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/user/blueprint.php b/panel/app/src/panel/models/user/blueprint.php new file mode 100644 index 0000000..ad09ca8 --- /dev/null +++ b/panel/app/src/panel/models/user/blueprint.php @@ -0,0 +1,59 @@ +user = $user; + + // load from yaml file + $this->load(); + + } + + public function load() { + + // get the user role and load the + // correspondant blueprint if available + $this->name = basename(strtolower($this->user->role())); + + // try to find a user blueprint + $file = kirby()->get('blueprint', 'users/' . $this->name); + + if($file) { + $this->file = $file; + $this->yaml = data::read($this->file, 'yaml'); + + // remove the broken first line + unset($this->yaml[0]); + } + + } + + public function fields() { + $fields = (array)a::get($this->yaml, 'fields', array()); + return new Fields($fields, $this->user); + } + + public function __toString() { + return $this->name; + } + +} diff --git a/panel/app/src/panel/models/user/history.php b/panel/app/src/panel/models/user/history.php new file mode 100644 index 0000000..9d9ea3d --- /dev/null +++ b/panel/app/src/panel/models/user/history.php @@ -0,0 +1,94 @@ +site()->user($user->username())) { + $this->user = $user; + } else { + throw new Exception('The user could not be found'); + } + } + + public function add($id) { + + if(is_a('Kirby\\Panel\\Models\\Page', $id)) { + $page = $id; + } else { + if(empty($id)) return false; + + try { + $page = panel()->page($id); + } catch(Exception $e) { + return false; + } + } + + $history = $this->get(); + + // remove existing entries + foreach($history as $key => $val) { + if($val->id() == $page->id()) unset($history[$key]); + } + + array_unshift($history, $page->id()); + $history = array_slice($history, 0, 5); + + try { + $this->user->update(array( + 'history' => $history + )); + } catch(Exception $e) { + + } + + } + + public function get() { + + $history = $this->user->__get('history'); + + if(empty($history) or !is_array($history)) { + return array(); + } + + $update = false; + $result = array(); + + foreach($history as $item) { + + try { + $result[] = panel()->page($item); + } catch(Exception $e) { + $update = true; + } + + } + + if($update) { + + $history = array_map(function($item) { + return $item->id(); + }, $result); + + try { + $this->user->update(array( + 'history' => $history + )); + } catch(Exception $e) { + + } + + } + + return $result; + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/roots.php b/panel/app/src/panel/roots.php new file mode 100644 index 0000000..60d8a7a --- /dev/null +++ b/panel/app/src/panel/roots.php @@ -0,0 +1,34 @@ +panel = $panel; + $this->index = $root; + $this->app = $root . DS . 'app'; + $this->assets = $root . DS . 'assets'; + + $this->config = $this->app . DS . 'config'; + $this->controllers = $this->app . DS . 'controllers'; + $this->collections = $this->app . DS . 'collections'; + $this->models = $this->app . DS . 'models'; + $this->fields = $this->app . DS . 'fields'; + $this->forms = $this->app . DS . 'forms'; + $this->translations = $this->app . DS . 'translations'; + $this->widgets = $this->app . DS . 'widgets'; + $this->layouts = $this->app . DS . 'layouts'; + $this->lib = $this->app . DS . 'lib'; + $this->topbars = $this->app . DS . 'topbars'; + $this->snippets = $this->app . DS . 'snippets'; + $this->views = $this->app . DS . 'views'; + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/search.php b/panel/app/src/panel/search.php new file mode 100644 index 0000000..f3f6f95 --- /dev/null +++ b/panel/app/src/panel/search.php @@ -0,0 +1,109 @@ +query = trim($query); + $this->pages = new Collection; + $this->users = new Collection; + + // temporary disable the search cache + $this->cache = cache::setup('mock'); + + // try { + // $root = kirby()->roots()->cache() . DS . 'search'; + // dir::make($root); + // $this->cache = cache::setup('file', array('root' => $root)); + // } catch(Exception $e) { + // $this->cache = cache::setup('session'); + // } + + $this->run(); + + } + + public function data() { + + $pages = $this->cache->get('pages'); + $users = $this->cache->get('users'); + + if(empty($pages)) { + $pages = array(); + foreach(panel()->site()->index() as $page) { + $pages[] = array( + 'title' => (string)$page->title(), + 'uri' => (string)$page->id(), + ); + } + $this->cache->set('pages', $pages); + } + + if(empty($users)) { + foreach(panel()->users() as $user) { + $users[] = array( + 'username' => $user->username(), + 'email' => $user->email() + ); + } + $this->cache->set('users', $users); + } + + return compact('pages', 'users'); + + } + + public function run() { + + if(empty($this->query) or str::length($this->query) <= 1) { + return false; + } + + $data = $this->data(); + + + foreach($data['pages'] as $page) { + if( + str::contains($page['title'], $this->query) or + str::contains($page['uri'], $this->query) + ) { + $this->pages->append($page['uri'], $page); + } + } + + foreach($data['users'] as $user) { + if( + str::contains($user['username'], $this->query) or + str::contains($user['email'], $this->query) + ) { + $this->users->append($user['username'], $user); + } + } + + $this->pages = $this->pages->limit(5); + $this->users = $this->users->limit(5); + + } + + public function pages() { + return $this->pages; + } + + public function users() { + return $this->users; + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/snippet.php b/panel/app/src/panel/snippet.php new file mode 100644 index 0000000..7ece8fe --- /dev/null +++ b/panel/app/src/panel/snippet.php @@ -0,0 +1,14 @@ +_root = panel::instance()->roots()->snippets(); + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/structure.php b/panel/app/src/panel/structure.php new file mode 100644 index 0000000..b57d3b2 --- /dev/null +++ b/panel/app/src/panel/structure.php @@ -0,0 +1,181 @@ +model = $model; + $this->id = 'structure_' . sha1($id); + $this->blueprint = $this->model->blueprint(); + + } + + public function forField($field) { + + if(method_exists($this->model, $field)) { + throw new Exception('The field name: ' . $field . ' cannot be used as it is reserved'); + } + + + $this->field = $field; + $this->config = $this->model->getBlueprintFields()->get($this->field); + + + if(is_a($this->model, 'Page')) { + $source = $this->model->content()->get($this->field); + $decode = true; + } else if(is_a($this->model, 'File')) { + $source = $this->model->meta()->get($this->field); + $decode = true; + } else if(is_a($this->model, 'User')) { + $source = $this->model->{$this->field}(); + $decode = false; + } else { + throw new Exception('Invalid model for structure field: ' . $this->field); + } + + $this->source = $decode ? (array)yaml::decode($source) : (array)$source; + $this->store = new Store($this, $this->source); + + return $this; + + } + + public function config() { + return $this->config; + } + + public function source() { + return $this->source; + } + + public function store() { + return $this->store; + } + + public function model() { + return $this->model; + } + + public function field() { + return $this->field; + } + + public function id() { + return $this->id; + } + + public function fields() { + + $fields = $this->config->fields(); + $fields = new Fields($fields, $this->model); + $fields = $fields->toArray(); + + // make sure that no unwanted options or fields + // are being included here + foreach($fields as $name => $field) { + + // remove all structure fields within structures + if($field['type'] == 'structure') { + unset($fields[$name]); + // convert title fields to normal text fields + } else if($field['type'] == 'title') { + $fields[$name]['type'] = 'text'; + // remove invalid buttons from textareas + } else if($field['type'] == 'textarea') { + $buttons = a::get($fields[$name], 'buttons'); + if(is_array($buttons)) { + foreach($buttons as $index => $value) { + if(in_array($value, array('link','email'))) { + unset($fields[$name]['buttons'][$index]); + } + } + } else if($buttons !== false) { + $fields[$name]['buttons'] = array('bold', 'italic'); + } + } + + } + + return $fields; + + } + + public function data() { + + $collection = new Collection($this->store()->data()); + $collection = $collection->map(function($item) { + return new Obj($item); + }); + + return $collection; + + } + + public function toObject($array) { + return is_array($array) ? new Obj($array) : false; + } + + public function find($id) { + return $this->toObject($this->store()->find($id)); + } + + public function reset() { + + if($this->field) { + return $this->store()->reset(); + } else { + foreach(s::get() as $key => $value) { + if(str::startsWith($key, $this->id)) { + s::remove($key); + } + } + } + + } + + public function delete($id = null) { + return $this->store()->delete($id); + } + + public function add($data = array()) { + return $this->store()->add($data); + } + + public function update($id, $data = array()) { + return $this->toObject($this->store()->update($id, $data)); + } + + public function sort($ids) { + return $this->store()->sort($ids); + } + + public function toArray() { + return $this->store()->toArray(); + } + + public function toYaml() { + return $this->store()->toYaml(); + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/structure/store.php b/panel/app/src/panel/structure/store.php new file mode 100644 index 0000000..5cb599e --- /dev/null +++ b/panel/app/src/panel/structure/store.php @@ -0,0 +1,185 @@ +structure = $structure; + $this->source = $source; + $this->id = $structure->id() . '_' . $structure->field(); + $this->age = time(); + + $this->sync(); + $this->init(); + } + + public function init() { + + $data = s::get($this->id()); + + if(!is_array($data)) { + $raw = (array)$this->source; + } else { + $raw = (array)s::get($this->id(), array()); + } + + $data = array(); + + foreach($raw as $row) { + + if(is_string($row)) { + continue; + } + + if(!isset($row['id'])) { + $row['id'] = str::random(32); + } + + $data[$row['id']] = $row; + + } + + $this->data = $data; + s::set($this->id, $this->data); + s::set($this->id . '_age', $this->age); + + } + + /** + * Resets store if necessary to stay in sync with content file + */ + public function sync() { + + $file = $this->structure->model()->textfile(); + $ageModel = f::exists($file) ? f::modified($file) : 0; + $ageStore = s::get($this->id() . '_age'); + + if($ageStore < $ageModel) { + $this->reset(); + $this->age = $ageModel; + } else { + $this->age = $ageStore; + } + + } + + public function id() { + return $this->id; + } + + public function data() { + return $this->data; + } + + public function find($id) { + return a::get($this->data, $id); + } + + public function add($data) { + + $data['id'] = str::random(32); + + $this->data[ $data['id'] ] = $data; + $this->save(); + + return $data['id']; + + } + + public function update($id, $data) { + + if($entry = a::get($this->data, $id)) { + + foreach($data as $key => $value) { + $entry[$key] = $value; + } + + $this->data[$id] = $entry; + $this->save(); + + return $entry; + + } else { + return false; + } + + } + + public function delete($id) { + + if(is_null($id)) { + $this->data = array(); + } else { + unset($this->data[$id]); + } + + $this->save(); + + return $this->data; + + } + + public function sort($ids) { + + $data = array(); + + foreach($ids as $id) { + if($item = $this->find($id)) { + $data[$id] = $item; + } + } + + $this->data = $data; + $this->save(); + + return $this->data; + + } + + public function toArray() { + $array = array_values($this->data); + $array = array_map(function($item) { + unset($item['id']); + return $item; + }, $array); + return $array; + } + + public function toYaml() { + return trim(yaml::encode($this->toArray())); + } + + public function save() { + + s::set($this->id, $this->data); + + // keep the changes for the page + if(is_a($this->structure->model(), 'page')) { + $this->structure->model()->changes()->update( + $this->structure->field(), + $this->toYaml() + ); + } + + } + + public function reset() { + return s::remove($this->id); + } + + +} \ No newline at end of file diff --git a/panel/app/src/panel/topbar.php b/panel/app/src/panel/topbar.php new file mode 100644 index 0000000..4f2169f --- /dev/null +++ b/panel/app/src/panel/topbar.php @@ -0,0 +1,117 @@ +view = $view; + + if(is_object($input) and method_exists($input, 'topbar')) { + $input->topbar($this); + } else { + + $class = is_object($input) ? str_replace('model', '', strtolower(get_class($input))) : (string)$input; + $file = panel()->roots()->topbars() . DS . str::lower($class) . '.php'; + + if(file_exists($file)) { + + $callback = require($file); + $callback($this, $input); + + } else { + throw new Exception(l('topbar.error.class.definition') . $class); + } + + } + + } + + public function append($url, $title) { + + $this->breadcrumb[] = array( + 'title' => $title, + 'url' => $url + ); + + } + + public function menu() { + return new Snippet('menu'); + } + + public function breadcrumb() { + return new Snippet('breadcrumb', array( + 'items' => $this->breadcrumb + )); + } + + public function message() { + + if($message = s::get('message') and is_array($message)) { + + $text = a::get($message, 'text'); + $type = a::get($message, 'type', 'notification'); + + $element = new Brick('div'); + $element->addClass('message'); + + if($type == 'error') { + $element->addClass('message-is-alert'); + } else { + $element->addClass('message-is-notice'); + } + + $element->append(function() use($text) { + $content = new Brick('span'); + $content->addClass('message-content'); + $content->text($text); + return $content; + }); + + $element->append(function() { + $toggle = new Brick('a'); + $toggle->attr('href', url::current()); + $toggle->addClass('message-toggle'); + $toggle->html('×'); + return $toggle; + }); + + s::remove('message'); + + return $element; + + } + + } + + public function render() { + + $element = new Brick('header', '', array('class' => 'topbar')); + $element->append($this->menu()); + $element->append($this->breadcrumb()); + $element->append($this->html); + $element->append(new Snippet('search')); + $element->append($this->message()); + + return $element; + + } + + public function __toString() { + return (string)$this->render(); + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/translation.php b/panel/app/src/panel/translation.php new file mode 100644 index 0000000..04f0477 --- /dev/null +++ b/panel/app/src/panel/translation.php @@ -0,0 +1,80 @@ + 'es_419', + 'no_nb' => 'nb', + 'cz' => 'cs' + ); + + public function __construct($panel, $code) { + + $this->panel = $panel; + $this->code = basename($code); + + // convert old codes + if(isset($this->map[$this->code])) { + $this->code = $this->map[$this->code]; + } + + // set the root for the translation directory + $this->root = $this->panel->roots()->translations() . DS . $this->code; + + if(!is_dir($this->root)) { + throw new Exception('The translation does not exist: ' . $this->code); + } + + if(!is_file($this->root . DS . 'package.json')) { + throw new Exception('The package.json is missing for the translation with code: ' . $this->code); + } + + if(!is_file($this->root . DS . 'core.json')) { + throw new Exception('The core.json is missing for the translation with code: ' . $this->code); + } + + } + + public function code() { + return $this->code; + } + + public function root() { + return $this->root; + } + + public function load() { + return l::$data = data::read($this->root . DS . 'core.json'); + } + + public function info() { + if(!is_null($this->info)) return $this->info; + return $this->info = new Obj(data::read($this->root . DS . 'package.json')); + } + + public function direction() { + $direction = $this->info()->direction(); + return $direction ? $direction : 'ltr'; + } + + public function __call($method, $args) { + return $this->info()->{$method}(); + } + + public function __toString() { + return $this->code; + } + +} diff --git a/panel/app/src/panel/upload.php b/panel/app/src/panel/upload.php new file mode 100644 index 0000000..1ec93bb --- /dev/null +++ b/panel/app/src/panel/upload.php @@ -0,0 +1,22 @@ + 'The file is missing', + static::ERROR_MISSING_TMP_DIR => 'The /tmp directory is missing on your server', + static::ERROR_FAILED_UPLOAD => 'The upload failed', + static::ERROR_PARTIAL_UPLOAD => 'The file has been only been partially uploaded', + static::ERROR_UNALLOWED_OVERWRITE => 'The file exists and cannot be overwritten', + static::ERROR_MAX_SIZE => 'The file is too big. The maximum size is ' . f::niceSize($this->maxSize()), + static::ERROR_MOVE_FAILED => 'The file could not be moved', + static::ERROR_UNACCEPTED => 'The file is not accepted by the server' + ); + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/urls.php b/panel/app/src/panel/urls.php new file mode 100644 index 0000000..5341164 --- /dev/null +++ b/panel/app/src/panel/urls.php @@ -0,0 +1,35 @@ +panel = $panel; + + // base url + $this->index = rtrim($this->panel->kirby()->urls()->index(), '/') . '/' . basename($root); + + // assets + $this->assets = $this->index . '/assets'; + $this->css = $this->assets . '/css'; + $this->js = $this->assets . '/js'; + $this->images = $this->assets . '/images'; + + // enable urls without rewriting + if(kirby()->option('rewrite') === false) { + $this->index .= '/index.php'; + } + + // shortcuts + $this->api = $this->index . '/api'; + $this->login = $this->index . '/login'; + $this->logout = $this->index . '/logout'; + $this->error = $this->index . '/error'; + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/view.php b/panel/app/src/panel/view.php new file mode 100644 index 0000000..d30f5bf --- /dev/null +++ b/panel/app/src/panel/view.php @@ -0,0 +1,40 @@ +_root = panel::instance()->roots()->views(); + $this->_file = $file; + $this->_data = $data; + } + + public function __set($key, $value) { + $this->_data[$key] = $value; + } + + public function render() { + $file = $this->_root . DS . str_replace('.', DS, $this->_file) . '.php'; + if(!file_exists($file)) throw new Exception(l('view.error.invalid') . $file); + return tpl::load($file, $this->_data); + } + + public function __toString() { + try { + return (string)$this->render(); + } catch(Exception $e) { + return $e->getMessage(); + } + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/widgets.php b/panel/app/src/panel/widgets.php new file mode 100644 index 0000000..c73d825 --- /dev/null +++ b/panel/app/src/panel/widgets.php @@ -0,0 +1,92 @@ +order = kirby()->option('panel.widgets'); + + $this->defaults(); + $this->custom(); + $this->sort(); + + } + + public function load($name) { + + if(!isset($this->available[$name])) { + return false; + } + + $dir = $this->available[$name]; + $file = $dir . DS . $name . '.php'; + + if(!file_exists($file)) { + return false; + } + + $widget = require($file); + + if(is_array($widget)) { + $this->append($name, $widget); + return $widget; + } else { + return false; + } + + } + + public function defaults() { + + $kirby = kirby(); + $root = panel()->roots()->widgets(); + + foreach(dir::read($root) as $dir) { + $kirby->registry->set('widget', $dir, $root . DS . $dir); + } + + } + + public function custom() { + + $kirby = kirby(); + $root = $kirby->roots()->widgets(); + + foreach(dir::read($root) as $dir) { + $kirby->registry->set('widget', $dir, $root . DS . $dir); + } + + } + + public function sort() { + + // load all widgets from the registry + $this->available = kirby()->registry()->get('widget'); + + // the license warning must always be included + $this->order['license'] = true; + + // append ordered widgets + foreach($this->order as $name => $add) { + if($add) { + $this->load($name); + } + unset($this->available[$name]); + } + + // append the unsorted widgets + foreach($this->available as $name => $dir) { + $this->load($name); + } + + } + +} \ No newline at end of file diff --git a/panel/app/topbars/error.php b/panel/app/topbars/error.php new file mode 100644 index 0000000..123b6eb --- /dev/null +++ b/panel/app/topbars/error.php @@ -0,0 +1,5 @@ +append('', l('error.headline')); +}; \ No newline at end of file diff --git a/panel/app/topbars/user.php b/panel/app/topbars/user.php new file mode 100644 index 0000000..b6d2a48 --- /dev/null +++ b/panel/app/topbars/user.php @@ -0,0 +1,13 @@ +append(purl('users'), l('users')); + + if($user === 'user') { + $topbar->append(purl('users/add'), l('users.index.add')); + } else { + $topbar->append($user->url(), $user->username()); + } + +}; \ No newline at end of file diff --git a/panel/app/translations/ar/core.json b/panel/app/translations/ar/core.json new file mode 100644 index 0000000..1f09f80 --- /dev/null +++ b/panel/app/translations/ar/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "إلغاء", + "add": "إضافة", + "addit": "إضافة وتعديل", + "save": "حفظ", + "saved": "تم الحفظ!", + "change": "تغيير", + "delete": "حذف", + "insert": "إدراج", + "ok": "موافق", + "routes.error.invalid": "رابط غير صالح للوحة التحكم", + "controller.error.invalid": "مراقب غير صالح", + "controller.error.action": "Invalid action", + "view.error.invalid": "Invalid view:", + "options.show": "عرض الخيارات", + "options.hide": "إخفاء الخيارات", + "installation": "التنصيب", + "installation.check.headline": "تنصيب لوحة كيربي", + "installation.check.text": "المشاكل التالية واجهت كيربي أثناء التنصيب…", + "installation.check.retry": "إعادة المحاولة", + "installation.check.error": "هناك بعض المشاكل!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "المجلد /site/accounts غير قابل للكتابة", + "installation.check.error.avatars": "المجلد /assets/avatars غير قابل للكتابة", + "installation.check.error.blueprints": "فضلا قم بإضافة المجلد /site/blueprints", + "installation.check.error.content": "مجلد المحتوى وجميع الملفات التي بداخله يجب أن تكون قابلة للكتابة.", + "installation.check.error.thumbs": "مجلد الصور المصغرة يجب أن يكون قابل للكتابة.", + "installation.signup.username.label": "أنشئ حسابك الأول", + "installation.signup.username.placeholder": "اسم المستخدم", + "installation.signup.email.label": "البريد الإلكتروني", + "installation.signup.email.placeholder": "mail@example.com", + "installation.signup.password.label": "كلمة المرور", + "installation.signup.language.label": "اللغة", + "installation.signup.button": "إنشاء الحساب", + "login": "تسجيل الدخول", + "login.welcome": "فضلاً سجل الدخول باستخدام حسابك", + "login.username.label": "اسم المستخدم", + "login.password.label": "كلمة المرور", + "login.error": "خطأ في اسم المستخدم أو كلمة المرور", + "login.button": "تسجيل الدخول", + "login.log.error.permissions": "Login log file is not writable.", + "logout": "Log out", + "topbar.error.class.definition": "Missing topbar definition for class:", + "dashboard": "لوحة التحكم", + "dashboard.index.pages.title": "الصفحات", + "dashboard.index.pages.edit": "تعديل", + "dashboard.index.pages.add": "إضافة", + "dashboard.index.site.title": "وصلة موقعك", + "dashboard.index.account.title": "حسابك", + "dashboard.index.account.edit": "تعديل", + "dashboard.index.metatags.title": "متغيرات الموقع", + "dashboard.index.metatags.edit": "تعديل", + "dashboard.index.history.title": "آخر تحديثاتك", + "dashboard.index.history.text": "سيتم عرض آخر صفحاتك المعدلة هنا لتسهيل وصولك إليها مجدداً.", + "dashboard.index.license.title": "Kirby license", + "dashboard.index.license.text": "It seems you are running Kirby on a public server without a valid license!\n\nPlease, support Kirby and (link: {buy} text: buy a license now)\n\nIf you already have a license key, just add it to your config file: (link: {docs} text: site/config/config.php)", + "metatags": "متغيرات الموقع", + "metatags.info": "معلومات عن Kirby ", + "metatags.license": "Kirby license", + "metatags.version.toolkit": "Toolkit version", + "metatags.version.kirby": "Kirby version", + "metatags.version.panel": "Panel version", + "metatags.back": "العودة إلى لوحة التحكم", + "metatags.files": "Site files", + "site.delete.error": "The site cannot be deleted", + "pages.show.settings": "إعدادات الصفحة", + "pages.show.preview": "عرض المعاينة", + "pages.show.template": "القالب", + "pages.show.changeurl": "تغيير الوصلة", + "pages.show.invisible": "Status: invisible", + "pages.show.visible": "Status: visible", + "pages.show.changes.text": "You have unsaved changes!", + "pages.show.changes.button": "Discard", + "pages.show.delete": "حذف هذه الصفحة", + "pages.show.subpages.title": "الصفحات", + "pages.show.subpages.edit": "تعديل", + "pages.show.subpages.add": "إضافة", + "pages.show.subpages.empty": "هذه الصفحة لا تحتوي على صفحات فرعية", + "pages.show.files.title": "الملفات", + "pages.show.files.edit": "تعديل", + "pages.show.files.add": "إضافة", + "pages.show.files.empty": "هذه الصفحة لا تحتوي على ملفات", + "pages.show.error.permissions.title": "هذه الصفحة غير قابلة للكتابة", + "pages.show.error.permissions.text": "فضلاً قم بفحص جميع التصاريح على مجلد المحتوى وجميع الملفات التي بداخله.", + "pages.show.error.permissions.retry": "إعادة المحاولة", + "pages.show.error.notitle.title": "هذا المخطط لا يحتوي على حقل العنوان،", + "pages.show.error.notitle.text": "فضلاً قم بإضافة حقل العنوان والمحاولة مرة أخرى", + "pages.show.error.notitle.retry": "إعادة المحاولة", + "pages.show.error.form": "فضلاً قم بإكمال جميع الحقول بشكل صحيح", + "pages.add.title.label": "إضافة صفحة جديدة", + "pages.add.title.placeholder": "العنوان", + "pages.add.url.label": "الوصلة", + "pages.add.url.enter": "(اكتب عنوانك)", + "pages.add.url.close": "إغلاق", + "pages.add.url.help": "الصيغة: حروف صغيرة a-z، أرقام 0-9 وعلامة الشرطة -.", + "pages.add.template.label": "القالب", + "pages.add.error.create": "The page could not be created", + "pages.add.error.title": "العنوان مفقود", + "pages.add.error.template": "القالب مفقود", + "pages.add.error.max.headline": "لا يُسمح بإضافة صفحات جديدة", + "pages.add.error.max.text": "تم استهلاك الحد الأقصى من الصفحات الفرعية لهذه الصفحة", + "pages.url.uid.label": "الوصلة", + "pages.url.uid.label.option": "إنشاء من العنوان", + "pages.url.error.exists": "توجد صفحة أخرى لها نفس الوصلة", + "pages.url.error.move": "لم يكن بالإمكان تغيير الوصلة", + "pages.url.error.rights": "You cannot change the URL of this page", + "pages.template.select.label": "القالب", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Position", + "pages.toggle.invisible": "invisible", + "pages.toggle.publish": "Do you really want to change the status of this page to **visible?**", + "pages.toggle.hide": "Do you really want to change the status of this page to **invisible?**", + "pages.toggle.error.error": "The status of the error page cannot be changed", + "pages.delete.headline": "هل أنت متأكد من رغبتك في حذف هذه الصفحة؟", + "pages.delete.error.home.headline": "لا يمكن حذف الصفحة الرئيسة", + "pages.delete.error.home.text": "قمت بمحاولة حذف الصفحة الرئيسة. لا يمكن إتمام ذلك بسبب ما سيعقبه من آثار غير مرغوبة.", + "pages.delete.error.error.headline": "لا يمكن حذف صفحة الأخطاء", + "pages.delete.error.error.text": "قمت بمحاولة حذف صفحة الأخطاء. لا يمكن إتمام ذلك بسبب ما سيعقبه من آثار غير مرغوبة.", + "pages.delete.error.children.headline": "لا يمكن حذف هذه الصفحة", + "pages.delete.error.children.text": "لا يمكن حذف هذه الصفحة لإحتوائها على صفحات فرعية. لإتمام ذلك، عليك أن تحذف صفحاتها الفرعية أولاً..", + "pages.delete.error.blocked.headline": "لا يمكن حذف هذه الصفحة", + "pages.delete.error.blocked.text": "هذه الصفحة مقفلة ولا يمكن حذفها.", + "pages.search.help": "ابحث عن الصفحات باستخدام الوصلة. تنقل بين نتائج البحث باستخدام مفاتيح الأسهم لأعلى وأسفل، واضغط مفتاح الإدخال للإنتقال للصفحة المحددة.", + "pages.search.noresults": "لا توجد نتائج لما بحثت عنه. فضلاً حاول البحث مرة أخرى بوصلة مختلفة.", + "pages.error.missing": "لم يتم العثور على الصفحة.", + "subpages": "الصفحات", + "subpages.index.headline": "الصفحات في", + "subpages.index.back": "عودة", + "subpages.index.add": "إضافة صفحة جديدة", + "subpages.index.add.first.text": "هذه الصفحة لا تملك صفحات فرعية حالياً", + "subpages.index.add.first.button": "أضف الصفحة الأولى", + "subpages.index.visible": "الصفحات المرئية", + "subpages.index.visible.help": "اسحب الصفحات المخفية إلى هنا لترتيبها أو جعلها مرئية.", + "subpages.index.invisible": "الصفحات المخفية", + "subpages.index.invisible.help": "اسحب الصفحات المرئية إلى هنا لبعثرتها أو جعلها مخفية.", + "subpages.add.error": "This page is not allowed to have subpages", + "subpages.add.error.more": "This page cannot have any more subpages", + "subpages.error.missing": "لم يتم العثور على الصفحة", + "files": "Files", + "files.index.headline": "الملفات لـ", + "files.index.back": "عودة", + "files.index.upload": "رفع ملف جديد", + "files.index.upload.first.text": "هذه الصفحة لا تملك ملفات حالياً", + "files.index.upload.first.button": "ارفع الملف الأول", + "files.index.edit": "تعديل", + "files.index.delete": "حذف", + "files.index.error.disabled": "The page is not allowed to have any files", + "files.add.error.max": "The maximum number of files for the current page has been reached.", + "files.add.error.extension.missing": "You cannot upload files without extension", + "files.add.error.extension.forbidden": "Forbidden file extension", + "files.add.error.mime.forbidden": "Forbidden mime type", + "files.add.error.htaccess": "htaccess files cannot be uploaded", + "files.add.error.invisible": "Invisible files cannot be uploaded", + "files.add.blueprint.type.error": "Page only allows:", + "files.add.blueprint.size.error": "Page only allows file size of", + "files.show.name.label": "اسم الملف", + "files.show.info.label": "النوع / الحجم / الأبعاد", + "files.show.link.label": "الوصلة العلنية", + "files.show.open": "عرض/تنزيل الملف", + "files.show.back": "عودة", + "files.show.replace": "استبدال", + "files.show.delete": "حذف", + "files.show.error.rename": "لم يكن بالإمكان تغيير اسم الملف", + "files.show.error.form": "فضلاً قم بتعبئة جميع الحقول بشكل صحيح", + "files.upload.drop": "اسقط الملفات هنا…", + "files.upload.click": "…او اضغط للرفع", + "files.replace.drop": "اسقط ملفاً هنا…", + "files.replace.click": "…او اضغط للاستبدال", + "files.replace.error.type": "الملف المرفوع يجب أن يكون من نفس النوع", + "files.delete.headline": "هل أنت متأكد من رغبتك في حذف هذا الملف؟", + "files.error.missing.page": "لم يتم العثور على الصفحة", + "files.error.missing.file": "لم يتم العثور على الملف", + "users": "المستخدمون", + "users.index.headline": "جميع المستخدمين", + "users.index.add": "إضافة مستخدم جديد", + "users.index.edit": "تعديل", + "users.index.delete": "حذف", + "users.form.username.label": "اسم المستخدم", + "users.form.username.placeholder": "اسم المستخدم الخاص بك", + "users.form.username.help": "الصيغة: حروف صغيرة a-z، أرقام 0-9 وعلامة الشرطة -.", + "users.form.username.readonly": "لا يمكن تغيير اسم المستخدم", + "users.form.firstname.label": "الاسم", + "users.form.lastname.label": "العائلة", + "users.form.email.label": "البريد الإلكتروني", + "users.form.email.placeholder": "mail@example.com", + "users.form.password.label": "كلمة المرور", + "users.form.password.confirm.label": "تأكيد كلمة المرور", + "users.form.password.new.label": "كلمة المرور الجديدة", + "users.form.password.new.confirm.label": "تأكيد كلمة المرور الجديدة", + "users.form.password.new.help": "اترك الحقل فارغاً لإبقاء كلمة المرور الحالية", + "users.form.language.label": "اللغة", + "users.form.role.label": "الوظيفة", + "users.form.options.headline": "خيارات الحساب", + "users.form.options.message": "إرسال بريد إلكتروني", + "users.form.options.delete": "حذف الحساب", + "users.form.avatar.headline": "الصورة الشخصية", + "users.form.avatar.upload": "رفع الصورة الشخصية", + "users.form.avatar.replace": "استبدال الصورة الشخصية", + "users.form.avatar.delete": "حذف الصورة الشخصية", + "users.form.back": "العودة إلى المستخدمين", + "users.form.error.password.confirm": "فضلاً قم بتأكيد كلمة المرور", + "users.form.error.update": "لم يكن بالإمكان تحديث المستخدم", + "users.form.error.update.rights": "You are not allowed to update this user", + "users.form.error.create": "لم يكن بالإمكان إنشاء المستخدم", + "users.form.error.permissions.title": "مجلد الحسابات غير قابل للكتابة", + "users.form.error.permissions.text": "تأكد أن المجلد /site/accounts موجود وقابل للكتابة.", + "users.delete.headline": "هل أنت متأكد من رغبتك في حذف هذا المستخدم؟", + "users.delete.error": "لم يكن بالإمكان حذف المستخدم", + "users.delete.error.permission": "You are not allowed to delete users", + "users.delete.error.permission.single": "You are not allowed to delete this user", + "users.delete.error.lastadmin": "You cannot delete the last admin", + "users.avatar.drop": "اسقط الصورة الشخصية هنا…", + "users.avatar.click": "…او اضغط للرفع", + "users.avatar.error.type": "بإمكانك رفع الصورة فقط بالصيغ JPG، أو PNG، أو GIF.", + "users.avatar.error.folder.headline": "مجلد الصور الشخصية غير قابل للكتابة", + "users.avatar.error.folder.text": "تأكد أن من إنشاء المجلد /assets/avatars وكونه قابل للكتابة لترفع الصور الشخصية.", + "users.avatar.error.permission": "You are not allowed to change the avatar", + "users.avatar.delete.error": "لم يكن بالإمكان حذف الصورة الشخصية", + "users.avatar.delete.error.permission": "You are not allowed to delete the avatar of this user", + "users.avatar.delete.success": "تم حذف الصورة الشخصية", + "users.avatar.missing": "This user has no avatar", + "users.error.missing": "لم يتم العثور على المستخدم", + "user.error.lastadmin": "You are the only admin. This cannot be changed.", + "form.error.missing": "The form cannot be found", + "form.construct.error.invalid": "Invalid form construction method", + "fields.required": "إلزامي", + "fields.date.label": "التاريخ", + "fields.date.months": [ + "يناير", + "فبراير", + "مارس", + "أبريل", + "مايو", + "يونيو", + "يوليو", + "أغسطس", + "سبتمبر", + "أكتوبر", + "نوڤمبر", + "ديسمبر" + ], + "fields.date.weekdays": [ + "الأحد", + "الإثنين", + "الثلاثاء", + "الأربعاء", + "الخميس", + "الجمعة", + "السبت" + ], + "fields.date.weekdays.short": [ + "أحد", + "أثنين", + "ثلاثاء", + "أربعاء", + "خميس", + "جمعة", + "سبت" + ], + "fields.email.label": "بريد إلكتروني", + "fields.email.placeholder": "mail@example.com", + "fields.number.label": "رقم", + "fields.number.placeholder": "#", + "fields.page.label": "صفحة", + "fields.page.placeholder": "محل/الصفحة/المرادة", + "fields.password.label": "كلمة مرور", + "fields.structure.add": "إضافة", + "fields.structure.add.first": "إضافة السجل الأول", + "fields.structure.empty": "لا توجد سجلات حالياً.", + "fields.structure.entry.error": "The item could not be found", + "fields.structure.cancel": "إلغاء", + "fields.structure.save": "حفظ", + "fields.structure.edit": "تعديل", + "fields.structure.delete": "حذف", + "fields.structure.delete.label": "Do you really want to delete this entry?", + "fields.tags.label": "وسوم", + "fields.tel.label": "هاتف", + "fields.textarea.buttons.bold.label": "نص عريض", + "fields.textarea.buttons.bold.text": "نص عريض", + "fields.textarea.buttons.italic.label": "نص مائل", + "fields.textarea.buttons.italic.text": "نص مائل", + "fields.textarea.buttons.link.label": "وصلة", + "fields.textarea.buttons.email.label": "بريد إلكتروني", + "fields.textarea.buttons.image.label": "صورة", + "fields.textarea.buttons.file.label": "ملف", + "fields.toggle.yes": "نعم", + "fields.toggle.no": "لا", + "fields.toggle.on": "تشغيل", + "fields.toggle.off": "تعطيل", + "fields.error.missing.controller": "The field controller file is missing", + "fields.error.missing.class": "The field controller class is missing", + "fields.error.route.invalid": "Invalid field route", + "fields.error.extended": "The field cannot be extended", + "editor.link.url.label": "إضافة وصلة", + "editor.link.text.label": "نص الوصلة", + "editor.link.text.help": "نص الوصلة اختياري", + "editor.email.address.label": "أدخل بريد إلكتروني", + "editor.email.address.placeholder": "mail@example.com", + "editor.email.text.label": "نص الوصلة", + "editor.email.text.help": "نص الوصلة اختياري", + "editor.file.empty": "هذه الصفحة لا تملك ملفات", + "editor.image.empty": "هذه الصفحة لا تملك صوراً", + "autocomplete.method.error": "Invalid autocomplete method", + "blueprints.error.default.missing": "Missing default blueprint", + "error": "خطأ", + "error.headline": "خطأ" +} \ No newline at end of file diff --git a/panel/app/translations/ar/package.json b/panel/app/translations/ar/package.json new file mode 100644 index 0000000..3765a0a --- /dev/null +++ b/panel/app/translations/ar/package.json @@ -0,0 +1,6 @@ +{ + "title": "العربية", + "direction": "rtl", + "author": "أحمد الحداد ", + "version": "1.0.0" +} \ No newline at end of file diff --git a/panel/app/translations/bg/core.json b/panel/app/translations/bg/core.json new file mode 100644 index 0000000..606f188 --- /dev/null +++ b/panel/app/translations/bg/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Откажи", + "add": "Добави", + "addit": "Добави & Редактирай", + "save": "Запиши", + "saved": "Записано", + "change": "Промени", + "delete": "Изтрий", + "insert": "Вмъкни", + "ok": "Ок", + "routes.error.invalid": "Невалиден URL на Панел", + "controller.error.invalid": "Невалиден контролер", + "controller.error.action": "Невалидно действие", + "view.error.invalid": "Невалиден изглед:", + "options.show": "Покажи Опции", + "options.hide": "Скрий Опции", + "installation": "Инсталация", + "installation.check.headline": "Инсталиране на Kirby Панел", + "installation.check.text": "Kirby откри следните проблеми по време на инсталацията...", + "installation.check.retry": "Опитай пак", + "installation.check.error": "Има няколко проблема!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "папката /site/accounts не е записваема", + "installation.check.error.avatars": "Папката assets/avatars не е записваема", + "installation.check.error.blueprints": "моля добавете папка /site/blueprints", + "installation.check.error.content": "Папката \"content\" и всички файлове в нея трябва да бъдат записваеми", + "installation.check.error.thumbs": "Папката \"thumbs\" трябва да бъде записваема.", + "installation.signup.username.label": "Създайте Вашия първи акаунт", + "installation.signup.username.placeholder": "Потребителско име", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@example.com", + "installation.signup.password.label": "Парола", + "installation.signup.language.label": "Език", + "installation.signup.button": "Създайте своя акаунт", + "login": "Подписване", + "login.welcome": "Please log in with your new account", + "login.username.label": "Потребителско име", + "login.password.label": "Парола", + "login.error": "Невалидно потребитебско име или парола", + "login.button": "Log in", + "login.log.error.permissions": "Лог файла за влизанията не е записваем", + "logout": "Log out", + "topbar.error.class.definition": "липсва topbar definition за клас:", + "dashboard": "Табло", + "dashboard.index.pages.title": "Страници", + "dashboard.index.pages.edit": "Редактирай", + "dashboard.index.pages.add": "Добави", + "dashboard.index.site.title": "URL на Вашия сайт", + "dashboard.index.account.title": "Вашия акаунт", + "dashboard.index.account.edit": "Редактирай", + "dashboard.index.metatags.title": "Опции за сайта", + "dashboard.index.metatags.edit": "Редактирай", + "dashboard.index.history.title": "Вашите последни промени", + "dashboard.index.history.text": "Вашите последно променяни страници ще се покажат тук, за да е по-лесно да ги намерите отново по-късно.", + "dashboard.index.license.title": "Лиценз за Kirby", + "dashboard.index.license.text": "Изглежда използвате Kirby на публичен сървър без да притежавате валиден лиценз\n\nМоля, подкрепете Kirby и (link: {buy} text: купете своя лиценз сега)\n\nАко вече имате лицензен ключ, просто го добавете във Вашия config файл (link: {docs} text: site/config/config.php)", + "metatags": "Опции за сайта", + "metatags.info": "Информация за Kirby", + "metatags.license": "Лиценз за Kirby", + "metatags.version.toolkit": "Toolkit версия", + "metatags.version.kirby": "Версия на Kirby", + "metatags.version.panel": "Версия на Панел", + "metatags.back": "Назад към Таблото", + "metatags.files": "Файлове на сайта", + "site.delete.error": "Сайтът не може да се изтрие", + "pages.show.settings": "Настройки за страницата", + "pages.show.preview": "Отвори предварителен преглед", + "pages.show.template": "Модел", + "pages.show.changeurl": "Промени URL", + "pages.show.invisible": "Статус: Невидимо", + "pages.show.visible": "Статус: Видимо", + "pages.show.changes.text": "Имате незаписани промени", + "pages.show.changes.button": "Отмени", + "pages.show.delete": "Изтрий тази страница", + "pages.show.subpages.title": "Страници", + "pages.show.subpages.edit": "Редактирай", + "pages.show.subpages.add": "Добави", + "pages.show.subpages.empty": "Тази страница няма под-страници", + "pages.show.files.title": "Файлове", + "pages.show.files.edit": "Редактирай", + "pages.show.files.add": "Добави", + "pages.show.files.empty": "Тази страница не съдържа файлове", + "pages.show.error.permissions.title": "Страницата не е записваема", + "pages.show.error.permissions.text": "Моля, проверете правата за content папката и всички файлове в нея", + "pages.show.error.permissions.retry": "Опитай пак", + "pages.show.error.notitle.title": "blueprint няма поле title", + "pages.show.error.notitle.text": "Моля добавете заглавно поле и опитайте отново", + "pages.show.error.notitle.retry": "Опитай пак", + "pages.show.error.form": "Моля, попълнете всички полета правилно", + "pages.add.title.label": "Добавете нова страница", + "pages.add.title.placeholder": "Заглавие", + "pages.add.url.label": "URL-добавка", + "pages.add.url.enter": "(въведете вашето заглавие)", + "pages.add.url.close": "Затвори", + "pages.add.url.help": "Формат: малки букви a-z, 0-9 и нормални тирета", + "pages.add.template.label": "Модел", + "pages.add.error.create": "Страницата не може да бъде създадена", + "pages.add.error.title": "Заглавието липсва", + "pages.add.error.template": "Моделът липсва", + "pages.add.error.max.headline": "Не са позволени нови страници", + "pages.add.error.max.text": "Максималният брой под-страници за тази страница е достигнат.", + "pages.url.uid.label": "URL-добавка", + "pages.url.uid.label.option": "Създайте от заглавието", + "pages.url.error.exists": "Страница със същата URL-добавка вече съществува", + "pages.url.error.move": "Добавката не може да се променя", + "pages.url.error.rights": "Не можете да смените URL за тази страница", + "pages.template.select.label": "Модел", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Позиция", + "pages.toggle.invisible": "Невидим", + "pages.toggle.publish": "Наистина ли искате да смените статуса на тази страница на **Видим?**", + "pages.toggle.hide": "Наистина ли искате да смените статуса на тази страница на **Невидим?**", + "pages.toggle.error.error": "Статуса на ERROR страницата не може да се променя", + "pages.delete.headline": "Наистина ли искате да изтриете тази страница?", + "pages.delete.error.home.headline": "HOME страницата не може да се изтрива", + "pages.delete.error.home.text": "Вие се опитвате да изтриете HOME страницата. Това не е възможно, защото би довело до нежелани резултати.", + "pages.delete.error.error.headline": "ERROR страницата не може да се изтрива", + "pages.delete.error.error.text": "Вие се опитвате да изтриете ERROR страницата. Това не е възможно, защото би довело до нежелани резултати.", + "pages.delete.error.children.headline": "Страницата не може да се изтрива", + "pages.delete.error.children.text": "Тази страница има под-страници и не може да се изтрие. Моля, изтрийте всички под-страници.", + "pages.delete.error.blocked.headline": "Страницата не може да се изтрие", + "pages.delete.error.blocked.text": "Тази страница е заключена и не може да се изтрие.", + "pages.search.help": "Търсете страници по URL. Използвайте клавишите Нагоре и Надолу за да обходите резултатите от търсенето. Използвайте Enter за да отидете в избраната страница.", + "pages.search.noresults": "Няма резултати от вашето търсене. Моля опитайте отново с различен URL.", + "pages.error.missing": "Страницата не може да бъде намерена", + "subpages": "Страници", + "subpages.index.headline": "Страници в", + "subpages.index.back": "Назад", + "subpages.index.add": "Добавете нова страница", + "subpages.index.add.first.text": "Тази страница все още няма под-страници", + "subpages.index.add.first.button": "Добавете първата страница", + "subpages.index.visible": "Видими страници", + "subpages.index.visible.help": "Влачете невидимите страници тук за да ги сортирате / направите видими.", + "subpages.index.invisible": "Невидими страници", + "subpages.index.invisible.help": "Влачете видимите страници тук за да не ги сортирате / да ги направите невидими.", + "subpages.add.error": "Тази страница не може да има под-страници", + "subpages.add.error.more": "Тази страница не може да има повече под-страници", + "subpages.error.missing": "Страницата не може да бъде намерена", + "files": "Файлове", + "files.index.headline": "Файлове за", + "files.index.back": "Назад", + "files.index.upload": "Качете нов файл", + "files.index.upload.first.text": "Тази страница все още няма файлове", + "files.index.upload.first.button": "Качете първия файл", + "files.index.edit": "Редактирай", + "files.index.delete": "Изтрий", + "files.index.error.disabled": "Тази страница не може да има файлове", + "files.add.error.max": "Максималният брой файлове за тази страница бе достигнат.", + "files.add.error.extension.missing": "Не можете да качвате файлове без разширение", + "files.add.error.extension.forbidden": "Забранено файлово разширение", + "files.add.error.mime.forbidden": "Забранен mime type", + "files.add.error.htaccess": "htaccess файл не може да се качи", + "files.add.error.invisible": "Невидимите файлове не могат да бъдат качени", + "files.add.blueprint.type.error": "Страницата позволява:", + "files.add.blueprint.size.error": "Страницата позволява големина на файла ", + "files.show.name.label": "Име на файла", + "files.show.info.label": "Тип / Големина / Размери", + "files.show.link.label": "Публична връзка", + "files.show.open": "Покажи / Свали файла", + "files.show.back": "Назад", + "files.show.replace": "Замести", + "files.show.delete": "Изтрий", + "files.show.error.rename": "Файлът не може да се преименува", + "files.show.error.form": "Моля, попълнете всички полета правилно", + "files.upload.drop": "Пуснете файловете тук...", + "files.upload.click": "... или кликнете за да качите", + "files.replace.drop": "Пуснете файл тук...", + "files.replace.click": "...или кликнете за да замените", + "files.replace.error.type": "Каченият файл трябва да има същия файлов тип", + "files.delete.headline": "Наистина ли искате да изтриете този файл?", + "files.error.missing.page": "Страницата не може да бъде намерена", + "files.error.missing.file": "Файлът не може да бъде намерен", + "users": "Потребители", + "users.index.headline": "Всички потребители", + "users.index.add": "Добавете нов потребител", + "users.index.edit": "Редактирай", + "users.index.delete": "Изтрий", + "users.form.username.label": "Потребителско име", + "users.form.username.placeholder": "Вашето потребителско име", + "users.form.username.help": "Позволени символи: малки букви a-zq 0-9 и тирета", + "users.form.username.readonly": "Потребителското име не може да се променя", + "users.form.firstname.label": "Първо име", + "users.form.lastname.label": "Фамилия", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@example.com", + "users.form.password.label": "Парола", + "users.form.password.confirm.label": "Потвърдете паролата", + "users.form.password.new.label": "Нова парола", + "users.form.password.new.confirm.label": "Потвърдете новата парола", + "users.form.password.new.help": "Оставете празна за да запазите текущата парола", + "users.form.language.label": "Език", + "users.form.role.label": "Роля", + "users.form.options.headline": "Опции на акаунта", + "users.form.options.message": "Изпратете email", + "users.form.options.delete": "Изтрийте акаунт", + "users.form.avatar.headline": "Профилна картинка", + "users.form.avatar.upload": "Качете профилна картинка", + "users.form.avatar.replace": "Заменете профилната картинка", + "users.form.avatar.delete": "Изтрийте профилната картинка", + "users.form.back": "Назад към потребители", + "users.form.error.password.confirm": "Моля, потвърдете паролата", + "users.form.error.update": "Потребителят не може да бъде актуализиран", + "users.form.error.update.rights": "Не ви е позволено да се актуализира този потребител", + "users.form.error.create": "Потребителят не може да бъде създаден", + "users.form.error.permissions.title": "Папката accounts не е записваема", + "users.form.error.permissions.text": "Моля, уверете се, че /site/accounts съществува и е записваема", + "users.delete.headline": "Наистина ли искате да изтриете този потребител?", + "users.delete.error": "Потребителят не може да бъде изтрит", + "users.delete.error.permission": "Не ви е позволено да изтривате потребители", + "users.delete.error.permission.single": "Не е позволено да изтривате този потребител", + "users.delete.error.lastadmin": "Не можете да изтриете последния администратор", + "users.avatar.drop": "Пуснете профилната снимка тук...", + "users.avatar.click": "... или кликнете за да качите", + "users.avatar.error.type": "Можете да качвате само JPG, PNG и GIF файлове.", + "users.avatar.error.folder.headline": "Папката за аватари трябва да е записваема", + "users.avatar.error.folder.text": "Моля създайте папка assets/avatar и я направете записваема за да качвате профилни снимки.", + "users.avatar.error.permission": "Не можете да сменяте профилната снимка", + "users.avatar.delete.error": "Профилната снимка не може да се изтрие", + "users.avatar.delete.error.permission": "Не можете да изтриете профилната снимка на този потребител.", + "users.avatar.delete.success": "Профилната снимка беше изтрита", + "users.avatar.missing": "Този потребител няма профилна снимка", + "users.error.missing": "Потребителят не може да бъде намерен.", + "user.error.lastadmin": "Вие сте само администратор. Това не може да се промени.", + "form.error.missing": "Формата не може да се намери", + "form.construct.error.invalid": "Невалиден form construction method", + "fields.required": "Задължително", + "fields.date.label": "Дата", + "fields.date.months": [ + "Януари", + "Февруари", + "Март", + "Април", + "Май", + "Юни", + "Юли", + "Август", + "Септември", + "Октомври", + "Ноември", + "Декември" + ], + "fields.date.weekdays": [ + "Неделя", + "Понеделник", + "Вторник", + "Сряда", + "Четвъртък", + "Петък", + "Събота" + ], + "fields.date.weekdays.short": [ + "Нд", + "Пн", + "Вт", + "Ср", + "Чт", + "Пт", + "Сб" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@example.com", + "fields.number.label": "Число", + "fields.number.placeholder": "№", + "fields.page.label": "Страница", + "fields.page.placeholder": "път/към/страницата", + "fields.password.label": "Парола", + "fields.structure.add": "Добави", + "fields.structure.add.first": "Добавете първото въвеждане", + "fields.structure.empty": "Няма все още въвеждания", + "fields.structure.entry.error": "Това не може да бъде намерено", + "fields.structure.cancel": "Откажи", + "fields.structure.save": "Ок", + "fields.structure.edit": "Редактирай", + "fields.structure.delete": "Изтрий", + "fields.structure.delete.label": "Наистина ли искате да изтриете това вписване?", + "fields.tags.label": "Етикети", + "fields.tel.label": "Телефон", + "fields.textarea.buttons.bold.label": "Получер шрифт", + "fields.textarea.buttons.bold.text": "Получер шрифт", + "fields.textarea.buttons.italic.label": "Наклонен шрифт", + "fields.textarea.buttons.italic.text": "Наклонен шрифт", + "fields.textarea.buttons.link.label": "Връзка", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Изображение", + "fields.textarea.buttons.file.label": "Файл", + "fields.toggle.yes": "Да", + "fields.toggle.no": "Не", + "fields.toggle.on": "Вкл.", + "fields.toggle.off": "Изкл.", + "fields.error.missing.controller": "field controller файлът липсва", + "fields.error.missing.class": "field controller класът липсва", + "fields.error.route.invalid": "Невалиден field route", + "fields.error.extended": "Полето не може да бъде extended", + "editor.link.url.label": "Вмъкни URL", + "editor.link.text.label": "Текст връзка", + "editor.link.text.help": "Текстовия линк е по избор", + "editor.email.address.label": "Вмъкни email адрес", + "editor.email.address.placeholder": "mail@example.com", + "editor.email.text.label": "Текст връзка", + "editor.email.text.help": "Текстовия линк е по избор", + "editor.file.empty": "Тази страница не съдържа файлове", + "editor.image.empty": "Тази страница няма изображения", + "autocomplete.method.error": "Невалиден метод за автоматично довършване", + "blueprints.error.default.missing": "Липсва blueprint по подразбиране", + "error": "Грешка", + "error.headline": "Грешка" +} \ No newline at end of file diff --git a/panel/app/translations/bg/package.json b/panel/app/translations/bg/package.json new file mode 100644 index 0000000..dc59f74 --- /dev/null +++ b/panel/app/translations/bg/package.json @@ -0,0 +1,4 @@ +{ + "title": "Bulgarian", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/ca/core.json b/panel/app/translations/ca/core.json new file mode 100644 index 0000000..5b0c208 --- /dev/null +++ b/panel/app/translations/ca/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Cancel·lar", + "add": "Afegir", + "addit": "Afegir i Editar", + "save": "Desar", + "saved": "Desat!", + "change": "Canviar", + "delete": "Eliminar", + "insert": "Insertar", + "ok": "Ok", + "routes.error.invalid": "URL del \"Panel\" incorrecte", + "controller.error.invalid": "\"Controller\" incorrecte", + "controller.error.action": "Acció incorrecte", + "view.error.invalid": "Vista incorrecte:", + "options.show": "Mostrar opcions", + "options.hide": "Ocultar opcions", + "installation": "Isntal·lació", + "installation.check.headline": "Instal·lació de Kirby Panel", + "installation.check.text": "Kirby ha trobat els següents problemes durant l'instal·lació…", + "installation.check.retry": "Reintentar", + "installation.check.error": "Hi ha alguns problemes!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts no té permís d'escriptura", + "installation.check.error.avatars": "/assets/avatars no té permís d'escriptura", + "installation.check.error.blueprints": "Afegeix el directori /site/blueprints", + "installation.check.error.content": "El directori de contingut i tots els seus arxius i subdirectoris han de tenir permís d'escriptura.", + "installation.check.error.thumbs": "El directori de \"thumbs\" ha de tenir permís d'escriptura.", + "installation.signup.username.label": "Crea la teva primera compta", + "installation.signup.username.placeholder": "Nom d'usuari", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@exemple.com", + "installation.signup.password.label": "Contrasenya", + "installation.signup.language.label": "Idioma", + "installation.signup.button": "Crea la teva compta", + "login": "Entrar", + "login.welcome": "Entra amb la teva nova compta", + "login.username.label": "Nom d'usuari", + "login.password.label": "Contrasenya", + "login.error": "Nom d'usuari o contrasenya incorrectes", + "login.button": "Entrar", + "login.log.error.permissions": "L'arxiu de log de Login no té permís d'escriptura", + "logout": "Tancar sessió", + "topbar.error.class.definition": "La definició del topbar no s'ha trobat per a la classe:", + "dashboard": "Escriptori", + "dashboard.index.pages.title": "Pàgines", + "dashboard.index.pages.edit": "Editar", + "dashboard.index.pages.add": "Afegir", + "dashboard.index.site.title": "URL de la web", + "dashboard.index.account.title": "La teva compta", + "dashboard.index.account.edit": "Editar", + "dashboard.index.metatags.title": "Opcions de la web", + "dashboard.index.metatags.edit": "Editar", + "dashboard.index.history.title": "Últimes modificacions", + "dashboard.index.history.text": "Aquí es mostraran les últimes modificacions realitzades per facilitar el seu posterior accés.", + "dashboard.index.license.title": "Llicència Kirby", + "dashboard.index.license.text": "Sembla que estàs fent servir Kirby en un servidor públic sense una llicència vàlida!\n\nDona suport a Kirby i (link: {buy} text: compra una llicència ara)\n\nSi ja tens una llicència comprada, afegeix-la al arxiu de configuració: (link: {docs} text: site/config/config.php)", + "metatags": "Opcions de la web", + "metatags.info": "Informació Kirby", + "metatags.license": "Llicència Kirby", + "metatags.version.toolkit": "Versió del Toolkit", + "metatags.version.kirby": "Versió de Kirby", + "metatags.version.panel": "Versió del Panel", + "metatags.back": "Tornar a l'escriptori", + "metatags.files": "Arxius de la web", + "site.delete.error": "La web no es pot eliminar", + "pages.show.settings": "Configuració de la pàgina", + "pages.show.preview": "Obrir vista prèvia", + "pages.show.template": "Plantilla", + "pages.show.changeurl": "Canviar URL", + "pages.show.invisible": "Estat: invisible", + "pages.show.visible": "Estat: visible", + "pages.show.changes.text": "Hi ha canvis pendents de desar!", + "pages.show.changes.button": "Descartar", + "pages.show.delete": "Eliminar aquesta pàgina", + "pages.show.subpages.title": "Pàgines", + "pages.show.subpages.edit": "Editar", + "pages.show.subpages.add": "Afegir", + "pages.show.subpages.empty": "Aquesta pàgina no té cap subpàgina", + "pages.show.files.title": "Arxius", + "pages.show.files.edit": "Editar", + "pages.show.files.add": "Afegir", + "pages.show.files.empty": "Aquesta pàgina no té cap arxiu", + "pages.show.error.permissions.title": "Aquesta pàgina no té permís d'escriptura", + "pages.show.error.permissions.text": "Revisa els permisos del directori de contingut i de tots els seus arxius.", + "pages.show.error.permissions.retry": "Reintentar", + "pages.show.error.notitle.title": "El \"blueprint\" de la pàgina no té cap camp definit per al títol", + "pages.show.error.notitle.text": "Afegeix un camp de títol i torna a intentar-ho", + "pages.show.error.notitle.retry": "Reintentar", + "pages.show.error.form": "Completa tots els camps correctament.", + "pages.add.title.label": "Afegir una nova pàgina", + "pages.add.title.placeholder": "Títol", + "pages.add.url.label": "URL-apèndix", + "pages.add.url.enter": "(introdueix el teu títol)", + "pages.add.url.close": "Tancar", + "pages.add.url.help": "Format: minúscules a-z, 0-9 i guions regulars", + "pages.add.template.label": "Plantilla", + "pages.add.error.create": "La pàgina no pot crear", + "pages.add.error.title": "Falta el títol", + "pages.add.error.template": "La plantilla no s'ha trobat", + "pages.add.error.max.headline": "No es permés afegir noves pàgines", + "pages.add.error.max.text": "S'ha arribat al número màxim de subpàgines per aquesta pàgina.", + "pages.url.uid.label": "URL-apèndix", + "pages.url.uid.label.option": "Crear a partir del títol", + "pages.url.error.exists": "Ja existeix una altra pàgina amb el mateix apèndix", + "pages.url.error.move": "L'apèndix no es pot modificar", + "pages.url.error.rights": "No pots canviar la URL d'aquest pàgina", + "pages.template.select.label": "Plantilla", + "pages.template.warning.text": "Els següents camps canviaran quan canviïs de plantilla", + "pages.template.warning.removed": "Eliminar camps", + "pages.template.warning.replaced": "Reemplaçar camps", + "pages.template.warning.added": "Camps afegits", + "pages.template.error": "La plantilla d'aquesta pàgina no es pot canviar", + "pages.toggle.position": "Posició", + "pages.toggle.invisible": "invisible", + "pages.toggle.publish": "Estàs segur de canviar l'estat de la pàgina a **visible**?", + "pages.toggle.hide": "Estàs segur de canviar l'estat de la pàgina a **invisible**?", + "pages.toggle.error.error": "L'estat de la pàgina d'error no es pot modificar", + "pages.delete.headline": "Estàs segur d'eliminar aquesta pàgina?", + "pages.delete.error.home.headline": "La pàgina d'inici no es pot eliminar", + "pages.delete.error.home.text": "Estàs intentant eliminar la pàgina d'inici. Això no és possible, i donaria lloc a efectes no desitjats.", + "pages.delete.error.error.headline": "La pàgina d'error no es pot eliminar", + "pages.delete.error.error.text": "Estàs intentant eliminar la pàgina d'error. Això no és possible, i donaria lloc a efectes no desitjats.", + "pages.delete.error.children.headline": "La pàgina no es pot eliminar", + "pages.delete.error.children.text": "Aquesta pàgina té subpàgines i no es pot eliminar. Per eliminar-la, cal eliminar primer totes les seves subpàgines.", + "pages.delete.error.blocked.headline": "La pàgina no es pot eliminar", + "pages.delete.error.blocked.text": "Aquesta pàgina esta bloquejada i no es pot eliminar.", + "pages.search.help": "Cercar pàgines per URL. Navega a través dels resultats de la cerca amb les tecles de fletxa cap amunt i cap avall i prem enter per saltar a la pàgina seleccionada.", + "pages.search.noresults": "No hi ha resultats de la cerca realitzada. Intenta-ho de nou amb una altra URL.", + "pages.error.missing": "La pàgina no s'ha trobat", + "subpages": "Pàgines", + "subpages.index.headline": "Pàgines a", + "subpages.index.back": "Tornar", + "subpages.index.add": "Afegir una nova pàgina", + "subpages.index.add.first.text": "Aquesta pàgina encara no té subpàgines", + "subpages.index.add.first.button": "Afegeix la primera pàgina", + "subpages.index.visible": "Pàgines visibles", + "subpages.index.visible.help": "Arrossega les pàgines invisibles aquí per ordenar-les i fer-les visibles.", + "subpages.index.invisible": "Pàgines invisibles", + "subpages.index.invisible.help": "Arrossega les pàgines visibles aquí per fer-les invisibles.", + "subpages.add.error": "Aquesta pàgina no pot tenir subpàgines", + "subpages.add.error.more": "Aquesta pàgina no pot tenir més subpàgines", + "subpages.error.missing": "La pàgina no s'ha trobat", + "files": "Arxius", + "files.index.headline": "Arxius per a", + "files.index.back": "Tornar", + "files.index.upload": "Carregar un nou arxiu", + "files.index.upload.first.text": "Aquesta pàgina encara no té cap arxiu", + "files.index.upload.first.button": "Carregar el primer arxiu", + "files.index.edit": "Editar", + "files.index.delete": "Eliminar", + "files.index.error.disabled": "Aquesta pàgina no pot tenir cap arxiu", + "files.add.error.max": "S'ha arribat al número màxim d'arxius per aquesta pàgina.", + "files.add.error.extension.missing": "No pots carregar arxius sense extensió", + "files.add.error.extension.forbidden": "Extensión de l'arxiu prohibida", + "files.add.error.mime.forbidden": "Mime type de l'arxiu prohibit", + "files.add.error.htaccess": "L'arxiu htaccess no es pot carregar", + "files.add.error.invisible": "Els arxius invisibles no es pode carregar", + "files.add.blueprint.type.error": "La pàgina només accepta:", + "files.add.blueprint.size.error": "La pàgina només accepta arxius amb un volum de", + "files.show.name.label": "Nom de l'arxiu", + "files.show.info.label": "Tipus / Volum / Dimensions", + "files.show.link.label": "Enllaç públic", + "files.show.open": "Mostrar / Descarregar arxiu", + "files.show.back": "Tornar", + "files.show.replace": "Reemplaçar", + "files.show.delete": "Eliminar", + "files.show.error.rename": "L'arxiu no pot ser renombrat", + "files.show.error.form": "Completa tots els camps correctament.", + "files.upload.drop": "Arrossega els arxius aquí…", + "files.upload.click": "… o clica per carregar-los", + "files.replace.drop": "Arrossega l'arxiu aquí…", + "files.replace.click": "… o clica per reeplaçar-lo", + "files.replace.error.type": "L'arxiu carregat ha de ser del mateix tipus", + "files.delete.headline": "Estàs segur d'eliminar aquest arxiu?", + "files.error.missing.page": "La pàgina no s'ha trobat", + "files.error.missing.file": "L'arxiu no s'ha trobat", + "users": "Usuaris", + "users.index.headline": "Tots els usuaris", + "users.index.add": "Afegir un nou usuari", + "users.index.edit": "Editar", + "users.index.delete": "Eliminar", + "users.form.username.label": "Nom d'usuari", + "users.form.username.placeholder": "Nom d'usuari", + "users.form.username.help": "Caràcters permesos: minúscules a-z, 0-9 i guions regulars", + "users.form.username.readonly": "El nom d'usuari no es pot canviar", + "users.form.firstname.label": "Nom", + "users.form.lastname.label": "Cognom", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@exemple.com", + "users.form.password.label": "Contrasenya", + "users.form.password.confirm.label": "Confirmar contrasenya", + "users.form.password.new.label": "Nova contrasenya", + "users.form.password.new.confirm.label": "Confirmar la nova contrasenya", + "users.form.password.new.help": "Deixa en blanc per mantenir la contrasenya actual", + "users.form.language.label": "Idioma", + "users.form.role.label": "Rol", + "users.form.options.headline": "Opcions de la compta", + "users.form.options.message": "Enviar correu electrònic", + "users.form.options.delete": "Eliminar compte", + "users.form.avatar.headline": "Imatge del perfil", + "users.form.avatar.upload": "Carregar imatge del perfil", + "users.form.avatar.replace": "Reemplaçar imatge del perfil", + "users.form.avatar.delete": "Eliminar imatge del perfil", + "users.form.back": "Tornar als usuaris", + "users.form.error.password.confirm": "Confirma la contrasenya", + "users.form.error.update": "L'usuari no s'ha pogut actualitzar", + "users.form.error.update.rights": "No pots actualitzar aquest usuari", + "users.form.error.create": "L'usuari no es pot crear", + "users.form.error.permissions.title": "El directori de comptes no té permís d'escriptura", + "users.form.error.permissions.text": "Assegure't de que el directori /site/accounts existeix i té permís d'escriptura", + "users.delete.headline": "Estàs segur d'eliminar aquest usuari?", + "users.delete.error": "L'usuari no es pot eliminar", + "users.delete.error.permission": "No pots eliminar usuaris", + "users.delete.error.permission.single": "No pots eliminar aquest usuari", + "users.delete.error.lastadmin": "No es pot eliminar l'últim administrador", + "users.avatar.drop": "Arrossega la imatge del perfil aquí…", + "users.avatar.click": "… o clica per carregar-la", + "users.avatar.error.type": "Només pot carregar arxius JPG, PNG i GIF", + "users.avatar.error.folder.headline": "La carpeta avatar no té permís d'escriptura", + "users.avatar.error.folder.text": "Si us plau, creï la carpeta /assets/avatars i doneu-li permisos d'escriptura per a pujar fotos del perfil.", + "users.avatar.error.permission": "No pots modificar l'avatar", + "users.avatar.delete.error": "La imatge del perfil no s'ha pogut eliminar", + "users.avatar.delete.error.permission": "No pots eliminar l'avatar d'aquest usuari", + "users.avatar.delete.success": "La imatge del perfil ha estat eliminada", + "users.avatar.missing": "Aquest usuari no té avatar", + "users.error.missing": "L'usuari no s'ha trobat", + "user.error.lastadmin": "Ets l'únic usuari administrador. No es pot modificar.", + "form.error.missing": "El formulari no s'ha trobat", + "form.construct.error.invalid": "Métode de construcció del formulari incorrecte", + "fields.required": "Obligatori", + "fields.date.label": "Data", + "fields.date.months": [ + "Gener", + "Febrer", + "Març", + "Abril", + "Maig", + "Juny", + "Juliol", + "Agost", + "Setembre", + "Octubre", + "Novembre", + "Desembre" + ], + "fields.date.weekdays": [ + "Diumenge", + "Dilluns", + "Dimarts", + "Dimecres", + "Dijous", + "Divendres", + "Dissabte" + ], + "fields.date.weekdays.short": [ + "dg.", + "dl.", + "dt.", + "dc.", + "dj.", + "dv.", + "ds." + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@exemple.com", + "fields.number.label": "Número", + "fields.number.placeholder": "#", + "fields.page.label": "Pàgina", + "fields.page.placeholder": "ruta/a/pàgina", + "fields.password.label": "Contrasenya", + "fields.structure.add": "Afegir", + "fields.structure.add.first": "Afegir la primera entrada", + "fields.structure.empty": "Encara no hi ha entrades.", + "fields.structure.entry.error": "No s'ha trobat", + "fields.structure.cancel": "Cancel·lar", + "fields.structure.save": "Ok", + "fields.structure.edit": "Editar", + "fields.structure.delete": "Eliminar", + "fields.structure.delete.label": "Estàs segur d'eliminar aquesta entrada?", + "fields.tags.label": "Etiquetes", + "fields.tel.label": "Telèfon", + "fields.textarea.buttons.bold.label": "Texte negreta", + "fields.textarea.buttons.bold.text": "Texte negreta", + "fields.textarea.buttons.italic.label": "Texte cursiva", + "fields.textarea.buttons.italic.text": "Texte cursiva", + "fields.textarea.buttons.link.label": "Enllaç", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Imatge", + "fields.textarea.buttons.file.label": "Arxiu", + "fields.toggle.yes": "Sí", + "fields.toggle.no": "No", + "fields.toggle.on": "Encès", + "fields.toggle.off": "Apagat", + "fields.error.missing.controller": "El \"Controller\" del camp no s'ha trobat", + "fields.error.missing.class": "La classe del \"Controller\" del camp no s'ha trobat", + "fields.error.route.invalid": "Ruta del camp incorrecte", + "fields.error.extended": "El camp no es pot extendre", + "editor.link.url.label": "Inserta URL", + "editor.link.text.label": "Texte enllaçat", + "editor.link.text.help": "L'enllaç és opcional", + "editor.email.address.label": "Inserta adressa d'email", + "editor.email.address.placeholder": "mail@exemple.com", + "editor.email.text.label": "Texte enllaçat", + "editor.email.text.help": "L'enllaç és opcional", + "editor.file.empty": "Aquesta pàgina no té fitxers", + "editor.image.empty": "Aquesta pàgina no té imatges", + "autocomplete.method.error": "Mètode d'autocompletar incorrecte", + "blueprints.error.default.missing": "El \"blueprint\" per defecte no s'ha trobat", + "error": "Error", + "error.headline": "Error" +} \ No newline at end of file diff --git a/panel/app/translations/ca/package.json b/panel/app/translations/ca/package.json new file mode 100644 index 0000000..c1b8934 --- /dev/null +++ b/panel/app/translations/ca/package.json @@ -0,0 +1,4 @@ +{ + "title": "Catalan", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/cs/core.json b/panel/app/translations/cs/core.json new file mode 100644 index 0000000..437f35d --- /dev/null +++ b/panel/app/translations/cs/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Zrušit", + "add": "Přidat", + "addit": "Přidat a upravit", + "save": "Uložit", + "saved": "Uloženo!", + "change": "Změnit", + "delete": "Smazat", + "insert": "Vložit", + "ok": "Ok", + "routes.error.invalid": "Neplatná URL panelu", + "controller.error.invalid": "Neplatný controller", + "controller.error.action": "Neplatná akce", + "view.error.invalid": "Neplatný view:", + "options.show": "Zobrazit možnosti", + "options.hide": "Skrýt možnosti", + "installation": "Instalace", + "installation.check.headline": "Instalace Kirby Panelu", + "installation.check.text": "Kirby během instalace narazilo na následující problémy…", + "installation.check.retry": "Zkusit znovu", + "installation.check.error": "Nastaly nějaké problémy!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts není zapisovatelné", + "installation.check.error.avatars": "/assets/avatars není zapisovatelné", + "installation.check.error.blueprints": "Prosím přidejte složku /site/blueprints", + "installation.check.error.content": "Složka content a všechny soubory a složky v ní musí být zapisovatelné.", + "installation.check.error.thumbs": "Složka thumbs musí být zapisovatelná.", + "installation.signup.username.label": "Vytvořte svůj první účet", + "installation.signup.username.placeholder": "Uživatelské jméno", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@example.com", + "installation.signup.password.label": "Heslo", + "installation.signup.language.label": "Jazky", + "installation.signup.button": "Vytvořit účet", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Uživatelské jméno", + "login.password.label": "Heslo", + "login.error": "Chybné jméno nebo heslo", + "login.button": "Log in", + "login.log.error.permissions": "Log soubor pro přihlášení není zapisovatelný.", + "logout": "Log out", + "topbar.error.class.definition": "Chybějící topbar definice pro třídu:", + "dashboard": "Přehled", + "dashboard.index.pages.title": "Stránky", + "dashboard.index.pages.edit": "Upravit", + "dashboard.index.pages.add": "Přidat", + "dashboard.index.site.title": "URL vašeho webu", + "dashboard.index.account.title": "Váš účet", + "dashboard.index.account.edit": "Upravit", + "dashboard.index.metatags.title": "Parametry webu", + "dashboard.index.metatags.edit": "Upravit", + "dashboard.index.history.title": "Vaše poslední změny", + "dashboard.index.history.text": "Zde budou zobrazeny stránky, které jste naposledy měnili, abyste k nim měli snadný přístup.", + "dashboard.index.license.title": "Kirby licence", + "dashboard.index.license.text": "It seems you are running Kirby on a public server without a valid license!\n\nPlease, support Kirby and (link: {buy} text: buy a license now)\n\nIf you already have a license key, just add it to your config file: (link: {docs} text: site/config/config.php)", + "metatags": "Parametry webu", + "metatags.info": "Kirby informace", + "metatags.license": "Kirby licence", + "metatags.version.toolkit": "Verze Toolkitu", + "metatags.version.kirby": "Verze Kirby", + "metatags.version.panel": "Verze panelu", + "metatags.back": "Zpět na přehled", + "metatags.files": "Soubory webu", + "site.delete.error": "Tento web nemůže být smazán", + "pages.show.settings": "Nastavení stránky", + "pages.show.preview": "Otevřít náhled", + "pages.show.template": "Šablona", + "pages.show.changeurl": "Změnit URL", + "pages.show.invisible": "Status: neviditelný", + "pages.show.visible": "Status: viditelný", + "pages.show.changes.text": "Máte neuložené změny!", + "pages.show.changes.button": "Zahodit", + "pages.show.delete": "Smazat tuto stránku", + "pages.show.subpages.title": "Stránky", + "pages.show.subpages.edit": "Upravit", + "pages.show.subpages.add": "Přidat", + "pages.show.subpages.empty": "Tato strana nemá podstrany", + "pages.show.files.title": "Soubory", + "pages.show.files.edit": "Upravit", + "pages.show.files.add": "Přidat", + "pages.show.files.empty": "Tato strana nemá soubory", + "pages.show.error.permissions.title": "Tato strana není zapisovatelná", + "pages.show.error.permissions.text": "Prosím zkontrolujte nastavení složky content a všech jejích podsložek a souborů", + "pages.show.error.permissions.retry": "Zkusit znovu", + "pages.show.error.notitle.title": "Blueprint nemá položku title", + "pages.show.error.notitle.text": "Prosím přidejte title a zkuste to znovu", + "pages.show.error.notitle.retry": "Zkusit znovu", + "pages.show.error.form": "Prosím vyplňte správně všechny položky", + "pages.add.title.label": "Přidat novou stránku", + "pages.add.title.placeholder": "Název", + "pages.add.url.label": "Přípona URL", + "pages.add.url.enter": "(vložte svůj název)", + "pages.add.url.close": "Zavřit", + "pages.add.url.help": "Formát: malé a-z, 0-9 a pomlčky", + "pages.add.template.label": "Šablona", + "pages.add.error.create": "Stránka nemohla být vytvořena", + "pages.add.error.title": "Chybí název", + "pages.add.error.template": "Šablona chybí", + "pages.add.error.max.headline": "Nové stránky nejsou povoleny", + "pages.add.error.max.text": "Bylo dosaženo maximálního počtu podstran pro tuto stránku", + "pages.url.uid.label": "Přípona URL", + "pages.url.uid.label.option": "Vytvořit z názvu", + "pages.url.error.exists": "Stránka se stejnou příponou již existuje", + "pages.url.error.move": "Nepodařilo se změnit příponu", + "pages.url.error.rights": "Nemůžete změnit URL této stránky", + "pages.template.select.label": "Šablona", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Pozice", + "pages.toggle.invisible": "neviditelný", + "pages.toggle.publish": "Opravdu chcete změnit status této stránky na **viditelný?**", + "pages.toggle.hide": "Opravdu chcete změnit status této stránky na **neviditelný?**", + "pages.toggle.error.error": "Status této error stránky nemůže být změněn", + "pages.delete.headline": "Opravdu chcete smazat tuto stránku?", + "pages.delete.error.home.headline": "Úvodní stránka nemůže být smazána", + "pages.delete.error.home.text": "Snažíte se smazat úvodní stránka webu. To není možné a způsobilo by to nechtěné problémy.", + "pages.delete.error.error.headline": "Chybová stránka nemůže být smazána", + "pages.delete.error.error.text": "Snažíte se smazat chybovou stránku. To není možné a způsobilo by to nechtěné problémy.", + "pages.delete.error.children.headline": "Stránka nemůže být smazána", + "pages.delete.error.children.text": "Tato stránka má podstránky a proto nemůže být smazána. Nejdříve prosím smažte všechny podstránky.", + "pages.delete.error.blocked.headline": "TStránka nemůže být smazána", + "pages.delete.error.blocked.text": "Tato stránka je zamčená a proto nemůže být smazána.", + "pages.search.help": "Vyhledat stránku pomocí URL. Pro přechod mezi výsledky použijte šipky nahodu a dolů. Pomocí klávesy Enter pak stránku vyberte.", + "pages.search.noresults": "Pro váš dotaz nejsou žádné výsledky. Zkuste to znovu s jinou URL.", + "pages.error.missing": "Stránku se nepodařilo nalézt.", + "subpages": "Stránky", + "subpages.index.headline": "Stránky v", + "subpages.index.back": "Zpět", + "subpages.index.add": "Přidat novou stránku", + "subpages.index.add.first.text": "Tato stránka ještě nemá podstránky", + "subpages.index.add.first.button": "Přidat první stránku", + "subpages.index.visible": "Viditelné stránky", + "subpages.index.visible.help": "Přetáhněte sem neviditelné stránky, pokud je chcete řadit a udělat viditelné.", + "subpages.index.invisible": "Neviditelné stránky", + "subpages.index.invisible.help": "Přetáhněte sem viditelné stránky, pokud je chcete udělat neviditelné a přestat je řadit.", + "subpages.add.error": "Tato stránka nemůže mít podstrany", + "subpages.add.error.more": "Tato stránka nemůže mít žádné další podstrany", + "subpages.error.missing": "Stránku se nepodařilo nalézt", + "files": "Soubory", + "files.index.headline": "Soubory pro", + "files.index.back": "Zpět", + "files.index.upload": "Nahrát nový soubor", + "files.index.upload.first.text": "Tato stránka ješte nemá žádné soubory", + "files.index.upload.first.button": "Nahrajte první soubor", + "files.index.edit": "Upravit", + "files.index.delete": "Smazat", + "files.index.error.disabled": "Tato stránka nemůže mít soubory", + "files.add.error.max": "Bylo dosaženo maximálního počtu souborů pro tuto stránku", + "files.add.error.extension.missing": "Nemůžete nahrát soubor bez přípony", + "files.add.error.extension.forbidden": "Zakázaná přípona souboru", + "files.add.error.mime.forbidden": "Zakázaný mime typ", + "files.add.error.htaccess": "Soubory htaccess nemohou být nahrány", + "files.add.error.invisible": "Neviditelné soubory nemohou být nahrány", + "files.add.blueprint.type.error": "Stránka povoluje pouze:", + "files.add.blueprint.size.error": "Stránka povoluje pouze soubory velikosti", + "files.show.name.label": "Jméno souboru", + "files.show.info.label": "Typ / Velikost / Rozměry", + "files.show.link.label": "Veřejný odkaz", + "files.show.open": "Ukázat/stáhnout soubot", + "files.show.back": "Zpět", + "files.show.replace": "Nahradit", + "files.show.delete": "Smazat", + "files.show.error.rename": "Soubor se nepodařilo odstranit", + "files.show.error.form": "Prosím vyplňte správně všechny položky", + "files.upload.drop": "Sem přetáhněte soubory…", + "files.upload.click": "…nebo klikněte pro nahrání", + "files.replace.drop": "Sem přetáhněte soubor…", + "files.replace.click": "…nebo klikněte pro nahrazení", + "files.replace.error.type": "Nahraný soubor musí být stejného typu", + "files.delete.headline": "Opravdu chcete smazat tento soubor?", + "files.error.missing.page": "Stránku se nepodařilo nalézt", + "files.error.missing.file": "Soubor se nepodařilo nalézt", + "users": "Uživatelé", + "users.index.headline": "Všichni uživatelé", + "users.index.add": "Přidat nového uživatele", + "users.index.edit": "Upravit", + "users.index.delete": "Smazat", + "users.form.username.label": "Uživatelské jméno", + "users.form.username.placeholder": "Vaše uživatelské jméno", + "users.form.username.help": "Povolené znaky: malé a-z, 0-9 a pomlčky", + "users.form.username.readonly": "Uživatelské jméno nelze změnit", + "users.form.firstname.label": "Křestní jméno", + "users.form.lastname.label": "Příjmení", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@example.com", + "users.form.password.label": "Heslo", + "users.form.password.confirm.label": "Potvrdit heslo", + "users.form.password.new.label": "Nové heslo", + "users.form.password.new.confirm.label": "Potvrďte nové heslo", + "users.form.password.new.help": "Nechte prázdné pro zachování současného hesla", + "users.form.language.label": "Jazyk", + "users.form.role.label": "Role", + "users.form.options.headline": "Možnosti účtu", + "users.form.options.message": "Poslat email", + "users.form.options.delete": "Smazat účet", + "users.form.avatar.headline": "Profilový obrázek", + "users.form.avatar.upload": "Nahrát profilový obrázek", + "users.form.avatar.replace": "Nahradit profilový obrázek", + "users.form.avatar.delete": "Smazat profilový obrázek", + "users.form.back": "Zpět na uživatele", + "users.form.error.password.confirm": "Prosím potvrďte heslo", + "users.form.error.update": "Uživatel nemohl být aktualizován", + "users.form.error.update.rights": "Nemáte dovoleno měnit tohoto uživatele", + "users.form.error.create": "Uživatel nemohl být vytvořen", + "users.form.error.permissions.title": "Složka account není zapisovatelná", + "users.form.error.permissions.text": "Prosím ověřte že složka /site/accounts existuje a je zapisovatelná.", + "users.delete.headline": "Opravdu chcete smazat tohoto uživatele?", + "users.delete.error": "Uživatel nemohl být smazán", + "users.delete.error.permission": "Nemáte dovoleno mazat uživatele", + "users.delete.error.permission.single": "Nemáte dovoleno smazat tohoto uživatele", + "users.delete.error.lastadmin": "Nemůžete smazat posledního administrátora", + "users.avatar.drop": "Sem přetáhněte uživatelský obrázek…", + "users.avatar.click": "…nebo klikněte pro nahrání", + "users.avatar.error.type": "Můžete nahrát pouze soubory typu JPG, PNG a GIF", + "users.avatar.error.folder.headline": "Složka avatar není zapisovatelná", + "users.avatar.error.folder.text": "Prosím vytvořte složku /assets/avatars a udělejte jí zapisovatelnou, aby bylo možné nahrát obrázek.", + "users.avatar.error.permission": "Nemáte dovoleno měnit avatara", + "users.avatar.delete.error": "Nebylo možné smazat profilový obrázek", + "users.avatar.delete.error.permission": "Nemáte dovoleno smazat avatara tomuto uživateli", + "users.avatar.delete.success": "Profilový obrázek byl smazán", + "users.avatar.missing": "Tento uživatel žádného avatara nemá", + "users.error.missing": "Uživatele se nepodařilo nalézt", + "user.error.lastadmin": "Jste jediným administrátorem. Toto nelze změnit.", + "form.error.missing": "Formulář se nepovedlo nalézt", + "form.construct.error.invalid": "Neplatná konstrukční metoda formuláře", + "fields.required": "Vyžadované", + "fields.date.label": "Datum", + "fields.date.months": [ + "Leden", + "Únor", + "Březen", + "Duben", + "Květen", + "Červen", + "Červenec", + "Srpen", + "Září", + "Říjen", + "Listopad", + "Prosinec" + ], + "fields.date.weekdays": [ + "neděle", + "pondělí", + "úterý", + "středa", + "čtvrtek", + "pátek", + "sobota" + ], + "fields.date.weekdays.short": [ + "ne", + "po", + "út", + "st", + "čt", + "pá", + "so" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@example.com", + "fields.number.label": "Číslo", + "fields.number.placeholder": "#", + "fields.page.label": "Stránka", + "fields.page.placeholder": "cesta/ke/strance", + "fields.password.label": "Heslo", + "fields.structure.add": "Přidat", + "fields.structure.add.first": "Přidat první záznam", + "fields.structure.empty": "Zatím nejsou žádné záznamy.", + "fields.structure.entry.error": "Položku se nepodařilo najít", + "fields.structure.cancel": "Zrušit", + "fields.structure.save": "Uložit", + "fields.structure.edit": "Upravit", + "fields.structure.delete": "Smazat", + "fields.structure.delete.label": "Opravdu chcete smazat tento záznam?", + "fields.tags.label": "Štítky", + "fields.tel.label": "Telefon", + "fields.textarea.buttons.bold.label": "Tučný text", + "fields.textarea.buttons.bold.text": "Tučný text", + "fields.textarea.buttons.italic.label": "Kurzíva", + "fields.textarea.buttons.italic.text": "Kurzíva", + "fields.textarea.buttons.link.label": "Odkaz", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Obrázek", + "fields.textarea.buttons.file.label": "Soubor", + "fields.toggle.yes": "Ano", + "fields.toggle.no": "Ne", + "fields.toggle.on": "Zap", + "fields.toggle.off": "Vyp", + "fields.error.missing.controller": "Soubor controller pro toto políčko chybí", + "fields.error.missing.class": "Třída controller pro toto políčko chybí", + "fields.error.route.invalid": "Neplatná cesta políčka", + "fields.error.extended": "Toto políčko nemůže být rozšířeno", + "editor.link.url.label": "Cílové URL", + "editor.link.text.label": "Text odkazu", + "editor.link.text.help": "Text odkazu je nepovinný", + "editor.email.address.label": "Vložit emailovou adresu", + "editor.email.address.placeholder": "mail@example.com", + "editor.email.text.label": "Text odkazu", + "editor.email.text.help": "Text odkazu je nepovinný", + "editor.file.empty": "Tato stránka nemá žádné soubory", + "editor.image.empty": "Tato stránka nemá žádné obrázky", + "autocomplete.method.error": "Neplatná našeptávací metoda", + "blueprints.error.default.missing": "Chybí výchozí blueprint", + "error": "Chyba", + "error.headline": "Chyba" +} \ No newline at end of file diff --git a/panel/app/translations/cs/package.json b/panel/app/translations/cs/package.json new file mode 100644 index 0000000..ce3af3d --- /dev/null +++ b/panel/app/translations/cs/package.json @@ -0,0 +1,4 @@ +{ + "title": "Česky", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/da/core.json b/panel/app/translations/da/core.json new file mode 100644 index 0000000..0f78264 --- /dev/null +++ b/panel/app/translations/da/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Annuller", + "add": "Ny", + "addit": "Tilføj & Rediger", + "save": "Gem", + "saved": "Gemt!", + "change": "Ændre", + "delete": "Slet", + "insert": "Indsæt", + "ok": "Ok", + "routes.error.invalid": "Ugyldig Panel URL", + "controller.error.invalid": "Ugyldig controller", + "controller.error.action": "Ugyldig handling", + "view.error.invalid": "Ugyldig visning:", + "options.show": "Vis indstillinger", + "options.hide": "Skjul indstillinger", + "installation": "Installation", + "installation.check.headline": "Kirby Panel Installation", + "installation.check.text": "Kirby stødte på følgende problemer under installationen…", + "installation.check.retry": "Prøv igen", + "installation.check.error": "Der er nogle problemer!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts er ikke skrivbar", + "installation.check.error.avatars": "/assets/avatars er ikke skrivbar", + "installation.check.error.blueprints": "Tilføj venligst en /site/blueprints mappe", + "installation.check.error.content": "Content mappen samt alle underliggende filer og mapper skal være skrivbare.", + "installation.check.error.thumbs": "Thumbs mappen skal være skrivbar.", + "installation.signup.username.label": "Opret din første konto", + "installation.signup.username.placeholder": "Brugernavn", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@eksempel.dk", + "installation.signup.password.label": "Adgangskode", + "installation.signup.language.label": "Sprog", + "installation.signup.button": "Opret din konto", + "login": "Log ind", + "login.welcome": "Log ind med din nye konto", + "login.username.label": "Brugernavn", + "login.password.label": "Adgangskode", + "login.error": "Ugyldigt brugernavn eller adgangskode", + "login.button": "Log ind", + "login.log.error.permissions": "Login log fil er ikke skrivbar", + "logout": "Log ud", + "topbar.error.class.definition": "Mangler topbar definition for class:", + "dashboard": "Dashboard", + "dashboard.index.pages.title": "Sider", + "dashboard.index.pages.edit": "Rediger", + "dashboard.index.pages.add": "Tilføj", + "dashboard.index.site.title": "Dit websites URL", + "dashboard.index.account.title": "Din konto", + "dashboard.index.account.edit": "Rediger", + "dashboard.index.metatags.title": "Website indstillinger", + "dashboard.index.metatags.edit": "Rediger", + "dashboard.index.history.title": "Dine seneste opdateringer", + "dashboard.index.history.text": "Dine seneste opdaterede sider vil blive vist her, for at gøre det nemt at finde dem igen senere.", + "dashboard.index.license.title": "Kirby licens", + "dashboard.index.license.text": "Det ser ud til at du kører Kirby på en offentlig server uden en gyldig licens!\n\nStøt venligst Kirby og (link: {buy} text: køb en licens nu)\n\nHar du allerede en licens nøgle, kan du blot tilføje den i din config fil: (link: {docs} text: site/config/config.php)", + "metatags": "Website indstillinger", + "metatags.info": "Kirby info", + "metatags.license": "Kirby licens", + "metatags.version.toolkit": "Toolkit version", + "metatags.version.kirby": "Kirby version", + "metatags.version.panel": "Panel version", + "metatags.back": "Tilbage til dashboard", + "metatags.files": "Website filer", + "site.delete.error": "Sitet kan ikke slettes", + "pages.show.settings": "Side indstillinger", + "pages.show.preview": "Se eksempel", + "pages.show.template": "Skabelon", + "pages.show.changeurl": "Ændre URL", + "pages.show.invisible": "Status: usynlig", + "pages.show.visible": "Status: synlig", + "pages.show.changes.text": "Du har ugemte ændringer!", + "pages.show.changes.button": "Kassér", + "pages.show.delete": "Slet denne side", + "pages.show.subpages.title": "Sider", + "pages.show.subpages.edit": "Rediger", + "pages.show.subpages.add": "Tilføj", + "pages.show.subpages.empty": "Denne side har ingen undersider", + "pages.show.files.title": "Filer", + "pages.show.files.edit": "Rediger", + "pages.show.files.add": "Tilføj", + "pages.show.files.empty": "Denne side har ingen filer", + "pages.show.error.permissions.title": "Siden er ikke skrivbar", + "pages.show.error.permissions.text": "Kontroller venligst skriverettigheder for content mappen samt alle filer.", + "pages.show.error.permissions.retry": "Prøv igen", + "pages.show.error.notitle.title": "Dette blueprint har intet titel-felt", + "pages.show.error.notitle.text": "Tilføj venligst et titel-felt og prøv igen", + "pages.show.error.notitle.retry": "Prøv igen", + "pages.show.error.form": "Udfyld venligst alle felter korrekt", + "pages.add.title.label": "Tilføj en ny side", + "pages.add.title.placeholder": "Titel", + "pages.add.url.label": "URL-appendiks", + "pages.add.url.enter": "(indtast din titel)", + "pages.add.url.close": "Luk", + "pages.add.url.help": "Format: små bogstaver a-z, 0-9 samt almindelige bindestreger", + "pages.add.template.label": "Skabelon", + "pages.add.error.create": "Siden kunne ikke oprettes", + "pages.add.error.title": "Titlen mangler", + "pages.add.error.template": "Skabelonen manger", + "pages.add.error.max.headline": "Der tillades ikke nye sider", + "pages.add.error.max.text": "Det maksimale antal undersider for denne side er nået.", + "pages.url.uid.label": "URL-appendiks", + "pages.url.uid.label.option": "Generer udfra titel", + "pages.url.error.exists": "En side med samme appendiks eksisterer allerede", + "pages.url.error.move": "URL-appendikset kunne ikke ændres", + "pages.url.error.rights": "Du kan ikke ændre denne sides URL", + "pages.template.select.label": "Skabelon", + "pages.template.warning.text": "Følgende felter ændres, hvis du skifter skabelon", + "pages.template.warning.removed": "Fjernede felter", + "pages.template.warning.replaced": "Erstattede felter", + "pages.template.warning.added": "Tilføjede felter", + "pages.template.error": "Skabelonen for denne side kan ikke ændres", + "pages.toggle.position": "Position", + "pages.toggle.invisible": "usynlig", + "pages.toggle.publish": "Ønsker du virkelig at ændre denne sides status til **synlig?**", + "pages.toggle.hide": "Ønsker du virkelig at ændre denne sides status til **usynlig?**", + "pages.toggle.error.error": "Status for fejl-siden kan ikke ændres", + "pages.delete.headline": "Ønsker du virkelig at slette denne side?", + "pages.delete.error.home.headline": "Forsiden kunne ikke slettes", + "pages.delete.error.home.text": "Du forsøger at slette forsiden. Dette er ikke muligt da det ville lede til en uønsket oplevelse.", + "pages.delete.error.error.headline": "Fejlsiden kan ikke slettes", + "pages.delete.error.error.text": "Du forsøger at slette fejlsiden. Dette er ikke muligt da det ville lede til en uønsket oplevelse.", + "pages.delete.error.children.headline": "Siden kan ikke slettes", + "pages.delete.error.children.text": "Denne side har undersider og kan derfor ikke slettes. Slet venligst alle undersider først.", + "pages.delete.error.blocked.headline": "Siden kan ikke slettes", + "pages.delete.error.blocked.text": "Denne side er låst og kan derfor ikke slettes.", + "pages.search.help": "Søg efter sider udfra URL. Naviger igennem søgeresultater med dine op- og ned-piletaster og tryk enter for at hoppe til den valgte side.", + "pages.search.noresults": "Søgningen gav intet resultat. Prøv venligst igen med en anden URL.", + "pages.error.missing": "Siden kunne ikke findes", + "subpages": "Sider", + "subpages.index.headline": "Sider i", + "subpages.index.back": "Tilbage", + "subpages.index.add": "Tilføj en ny side", + "subpages.index.add.first.text": "Denne side har ingen undersider endnu", + "subpages.index.add.first.button": "Tilføj den første side", + "subpages.index.visible": "Synlige sider", + "subpages.index.visible.help": "Træk usynlige sider hertil for at sortere dem og gøre dem synlige.", + "subpages.index.invisible": "Usynlige sider", + "subpages.index.invisible.help": "Træk synlige sider hertil for at gøre dem usynlige.", + "subpages.add.error": "Denne side må ikke have undersider", + "subpages.add.error.more": "Denne side kan ikke have flere undersider", + "subpages.error.missing": "Siden kunne ikke findes", + "files": "Filer", + "files.index.headline": "Filer for", + "files.index.back": "Tilbage", + "files.index.upload": "Upload en ny fil", + "files.index.upload.first.text": "Denne side har ingen filer endnu", + "files.index.upload.first.button": "Upload den første fil", + "files.index.edit": "Rediger", + "files.index.delete": "Slet", + "files.index.error.disabled": "Denne side må ikke have filer", + "files.add.error.max": "Det maksimale antal filer for denne side er nået.", + "files.add.error.extension.missing": "Du kan ikke uploade filer uden fil-endelse", + "files.add.error.extension.forbidden": "Uacceptabel fil-endelse", + "files.add.error.mime.forbidden": "Uacceptabel fil-type", + "files.add.error.htaccess": "htaccess filer kan ikke uploades", + "files.add.error.invisible": "Usynlige filer kan ikke uploades", + "files.add.blueprint.type.error": "Siden tillader kun:", + "files.add.blueprint.size.error": "Siden tillader kun en fil-størrelse på:", + "files.show.name.label": "Filnavn", + "files.show.info.label": "Type / Vægt / Dimensioner", + "files.show.link.label": "Offentligt link", + "files.show.open": "Se og hent fil", + "files.show.back": "Tilbage", + "files.show.replace": "Erstat", + "files.show.delete": "Slet", + "files.show.error.rename": "Filen kunne ikke omdøbes", + "files.show.error.form": "Udfyld venligst alle felter korrekt", + "files.upload.drop": "Træk filer hertil…", + "files.upload.click": "…eller klik for at uploade", + "files.replace.drop": "Træk en fil hertil…", + "files.replace.click": "…eller klik for at erstatte", + "files.replace.error.type": "Den valgte fil skal være af samme filtype", + "files.delete.headline": "Ønsker du virkelig at slette denne fil?", + "files.error.missing.page": "Siden kunne ikke findes", + "files.error.missing.file": "Filen kunne ikke findes", + "users": "Brugere", + "users.index.headline": "Alle brugere", + "users.index.add": "Tilføj en ny bruger", + "users.index.edit": "Rediger", + "users.index.delete": "Slet", + "users.form.username.label": "Brugernavn", + "users.form.username.placeholder": "Dit brugernavn", + "users.form.username.help": "Tilladte tegn: små bogstaver a-z, 0-9 og bindestreger", + "users.form.username.readonly": "Brugernavnet kan ikke ændres", + "users.form.firstname.label": "Fornavn", + "users.form.lastname.label": "Efternavn", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@eksempel.dk", + "users.form.password.label": "Adgangskode", + "users.form.password.confirm.label": "Bekræft adgangskode", + "users.form.password.new.label": "Ny adgangskode", + "users.form.password.new.confirm.label": "Bekræft den nye adgangskode", + "users.form.password.new.help": "Lad stå tomt for at beholde den nuværende adgangskode", + "users.form.language.label": "Sprog", + "users.form.role.label": "Rolle", + "users.form.options.headline": "Konto instillinger", + "users.form.options.message": "Send email", + "users.form.options.delete": "Slet konto", + "users.form.avatar.headline": "Profilbillede", + "users.form.avatar.upload": "Upload profilbillede", + "users.form.avatar.replace": "Erstat profilbillede", + "users.form.avatar.delete": "Slet profilbillede", + "users.form.back": "Tilbage til brugere", + "users.form.error.password.confirm": "Bekræft venligst adgangskoden", + "users.form.error.update": "Brugeren kunne ikke redigeres", + "users.form.error.update.rights": "Du har ikke tilladelse til at opdatere denne bruger", + "users.form.error.create": "Brugeren kunne ikke oprettes", + "users.form.error.permissions.title": "Account mappen er ikke skrivbar", + "users.form.error.permissions.text": "Sørg venligst for at /site/accounts eksisterer og er skrivbar.", + "users.delete.headline": "Ønsker du virkelig at slette denne bruger?", + "users.delete.error": "Brugeren kunne ikke slettes", + "users.delete.error.permission": "Du har ikke tilladelse til at slette brugere", + "users.delete.error.permission.single": "Du har ikke tilladelse til at slette denne bruger", + "users.delete.error.lastadmin": "Du kan ikke slette den sidste admin", + "users.avatar.drop": "Træk et profilbillede hertil…", + "users.avatar.click": "…eller klik for at vælge", + "users.avatar.error.type": "Du kan kun uploade JPG, PNG og GIF filer", + "users.avatar.error.folder.headline": "Avatar mappen er ikke skrivbar", + "users.avatar.error.folder.text": "Opret venligst mappen /assets/avatars og sørg for at den er skrivbar for at kunne uploade profilbilleder.", + "users.avatar.error.permission": "Du har ikke tilladelse til at ændre avatar", + "users.avatar.delete.error": "Profilbilledet kunne ikke slettes", + "users.avatar.delete.error.permission": "Du har ikke tilladelse til at slette denne brugers avatar", + "users.avatar.delete.success": "Profilbilledet er nu slettet", + "users.avatar.missing": "Denne bruger har ikke nogen avatar", + "users.error.missing": "Brugeren kunne ikke findes", + "user.error.lastadmin": "Du er den eneste admin. Dette kan ikke ændres.", + "form.error.missing": "Formularen kunne ikke findes", + "form.construct.error.invalid": "Ugyldig formular construction method", + "fields.required": "Påkrævet", + "fields.date.label": "Dato", + "fields.date.months": [ + "Januar", + "Februar", + "Marts", + "April", + "Maj", + "Juni", + "Juli", + "August", + "September", + "Oktober", + "November", + "December" + ], + "fields.date.weekdays": [ + "Søndag", + "Mandag", + "Tirsdag", + "Onsdag", + "Torsdag", + "Fredag", + "Lørdag" + ], + "fields.date.weekdays.short": [ + "Søn", + "Man", + "Tir", + "Ons", + "Tor", + "Fre", + "Lør" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@eksempel.dk", + "fields.number.label": "Nummer", + "fields.number.placeholder": "#", + "fields.page.label": "Side", + "fields.page.placeholder": "sti/til/side", + "fields.password.label": "Adgangskode", + "fields.structure.add": "Tilføj", + "fields.structure.add.first": "Tilføj den første indtastning", + "fields.structure.empty": "Ingen indtastninger endnu.", + "fields.structure.entry.error": "Emnet blev ikke fundet", + "fields.structure.cancel": "Annuller", + "fields.structure.save": "Gem", + "fields.structure.edit": "Rediger", + "fields.structure.delete": "Slet", + "fields.structure.delete.label": "Ønsker du virkelig at slette denne indtastning?", + "fields.tags.label": "Tags", + "fields.tel.label": "Telefon", + "fields.textarea.buttons.bold.label": "Fed tekst", + "fields.textarea.buttons.bold.text": "Fed tekst", + "fields.textarea.buttons.italic.label": "Kursiv tekst", + "fields.textarea.buttons.italic.text": "Kursiv tekst", + "fields.textarea.buttons.link.label": "Link", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Billede", + "fields.textarea.buttons.file.label": "Fil", + "fields.toggle.yes": "Ja", + "fields.toggle.no": "Nej", + "fields.toggle.on": "Til", + "fields.toggle.off": "Fra", + "fields.error.missing.controller": "En field controller fil mangler", + "fields.error.missing.class": "En field controller class mangler", + "fields.error.route.invalid": "Ugyldig field route", + "fields.error.extended": "Field kan ikke blive extended", + "editor.link.url.label": "Indsæt URL", + "editor.link.text.label": "Link tekst", + "editor.link.text.help": "Link tekst er valgfri", + "editor.email.address.label": "Indsæt email adresse", + "editor.email.address.placeholder": "mail@eksempel.dk", + "editor.email.text.label": "Link tekst", + "editor.email.text.help": "Link tekst er valgfri", + "editor.file.empty": "Denne side har ingen filer", + "editor.image.empty": "Denne side har ingen billeder", + "autocomplete.method.error": "Ugyldig autocomplete method", + "blueprints.error.default.missing": "Mangler standard blueprint", + "error": "Fejl", + "error.headline": "Fejl" +} \ No newline at end of file diff --git a/panel/app/translations/da/package.json b/panel/app/translations/da/package.json new file mode 100644 index 0000000..24abc8f --- /dev/null +++ b/panel/app/translations/da/package.json @@ -0,0 +1,4 @@ +{ + "title": "Dansk", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/de/core.json b/panel/app/translations/de/core.json new file mode 100644 index 0000000..d442019 --- /dev/null +++ b/panel/app/translations/de/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Abbrechen", + "add": "Hinzufügen", + "addit": "Hinzufügen & Bearbeiten", + "save": "Speichern", + "saved": "Gespeichert!", + "change": "Ändern", + "delete": "Löschen", + "insert": "Einfügen", + "ok": "Ok", + "routes.error.invalid": "Ungültige Panel-URL", + "controller.error.invalid": "Ungültiger Controller", + "controller.error.action": "Ungültige Aktion", + "view.error.invalid": "Ungültiger View:", + "options.show": "Optionen einblenden", + "options.hide": "Optionen ausblenden", + "installation": "Installation", + "installation.check.headline": "Kirby Panel Installation", + "installation.check.text": "Kirby hat die folgenden Probleme festgestellt…", + "installation.check.retry": "Wiederholen", + "installation.check.error": "Es gibt einige Probleme!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts ist nicht beschreibbar", + "installation.check.error.avatars": "/assets/avatars ist nicht beschreibbar", + "installation.check.error.blueprints": "Bitte lege den Ordner /site/blueprints an", + "installation.check.error.content": "/content und alle Inhalte müssen beschreibbar sein.", + "installation.check.error.thumbs": "/thumbs muss vorhanden und beschreibbar sein.", + "installation.signup.username.label": "Erstelle den ersten Benutzer", + "installation.signup.username.placeholder": "Benutzername", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@beispiel.de", + "installation.signup.password.label": "Passwort", + "installation.signup.language.label": "Sprache", + "installation.signup.button": "Erstellen", + "login": "Anmelden", + "login.welcome": "Bitte melde dich mit deinem neuen Account an", + "login.username.label": "Benutzername", + "login.password.label": "Passwort", + "login.error": "Ungültiger Benutzername oder Passwort", + "login.button": "Anmelden", + "login.log.error.permissions": "Die Anmeldelog-Datei ist nicht beschreibbar.", + "logout": "Abmelden", + "topbar.error.class.definition": "Fehlende Topbar-Definition für Klasse:", + "dashboard": "Übersicht", + "dashboard.index.pages.title": "Seiten", + "dashboard.index.pages.edit": "Bearbeiten", + "dashboard.index.pages.add": "Hinzufügen", + "dashboard.index.site.title": "Seite", + "dashboard.index.account.title": "Dein Account", + "dashboard.index.account.edit": "Bearbeiten", + "dashboard.index.metatags.title": "Einstellungen", + "dashboard.index.metatags.edit": "Bearbeiten", + "dashboard.index.history.title": "Deine letzten Änderungen", + "dashboard.index.history.text": "Sobald du die ersten Änderungen an Seiten vornimmst, werden sie hier aufgelistet, um jeder Zeit schnell darauf zugreifen zu können.", + "dashboard.index.license.title": "Kirby Lizenz", + "dashboard.index.license.text": "Scheinbar nutzt Du Kirby auf einem öffentlichen Server ohne gültige Lizenz!\n\nBitte unterstütze Kirby und (link: {buy} text: kaufe jetzt eine Lizenz)\n\nWenn Du bereits einen Lizenzschlüssel hast, füge ihn zur Config-Datei hinzu: (link: {docs} text: site/config/config.php)", + "metatags": "Einstellungen", + "metatags.info": "Kirby Information", + "metatags.license": "Kirby Lizenz", + "metatags.version.toolkit": "Toolkit Version", + "metatags.version.kirby": "Kirby Version", + "metatags.version.panel": "Panel Version", + "metatags.back": "Zurück zur Übersicht", + "metatags.files": "Globale Dateien", + "site.delete.error": "Die Seite kann nicht gelöscht werden", + "pages.show.settings": "Seiteneinstellungen", + "pages.show.preview": "Seite öffnen", + "pages.show.template": "Vorlage", + "pages.show.changeurl": "URL ändern", + "pages.show.invisible": "Status: unsichtbar", + "pages.show.visible": "Status: sichtbar", + "pages.show.changes.text": "Du hast ungespeicherte Änderungen!", + "pages.show.changes.button": "Verwerfen", + "pages.show.delete": "Seite löschen", + "pages.show.subpages.title": "Seiten", + "pages.show.subpages.edit": "Bearbeiten", + "pages.show.subpages.add": "Hinzufügen", + "pages.show.subpages.empty": "Diese Seite hat keine Unterseiten", + "pages.show.files.title": "Dateien", + "pages.show.files.edit": "Bearbeiten", + "pages.show.files.add": "Hinzufügen", + "pages.show.files.empty": "Diese Seite hat keine Dateien", + "pages.show.error.permissions.title": "Die Seite ist nicht beschreibbar", + "pages.show.error.permissions.text": "Bitte überprüfe die Schreibrechte für /content und alle Inhalte", + "pages.show.error.permissions.retry": "Wiederholen", + "pages.show.error.notitle.title": "Das Blueprint hat kein Titelfeld", + "pages.show.error.notitle.text": "Bitte füge ein Titelfeld ein und versuche es erneut", + "pages.show.error.notitle.retry": "Wiederholen", + "pages.show.error.form": "Bitte fülle alle Felder vollständig und korrekt aus", + "pages.add.title.label": "Eine neue Seite hinzufügen", + "pages.add.title.placeholder": "Titel", + "pages.add.url.label": "URL-Anhang", + "pages.add.url.enter": "(URL eingeben)", + "pages.add.url.close": "Schließen", + "pages.add.url.help": "Format: Kleinbuchstaben a-z, 0-9 und Bindestriche", + "pages.add.template.label": "Vorlage", + "pages.add.error.create": "Die Seite konnte nicht hinzugefügt werden", + "pages.add.error.title": "Der Titel fehlt", + "pages.add.error.template": "Die Vorlage fehlt", + "pages.add.error.max.headline": "Keine weiteren Unterseiten zugelassen", + "pages.add.error.max.text": "Die maximale Anzahl an Unterseiten für die aktuelle Seite ist erreicht.", + "pages.url.uid.label": "URL-Anhang", + "pages.url.uid.label.option": "Aus Titel erzeugen", + "pages.url.error.exists": "Eine Seite mit dem selben Anhang besteht bereits.", + "pages.url.error.move": "Die URL konnte nicht geändert werden", + "pages.url.error.rights": "Du kannst die URL dieser Seite nicht ändern", + "pages.template.select.label": "Vorlage", + "pages.template.warning.text": "Folgende Felder werden sich ändern, wenn du die Vorlage wechselst. ", + "pages.template.warning.removed": "Gelöschte Felder", + "pages.template.warning.replaced": "Ersetzte Felder", + "pages.template.warning.added": "Hinzugefügte Felder", + "pages.template.error": "Die Vorlage für diese Seite kann nicht geändert werden", + "pages.toggle.position": "Position", + "pages.toggle.invisible": "unsichtbar", + "pages.toggle.publish": "Willst du den Status der Seite wirklich in **sichtbar** umändern?", + "pages.toggle.hide": "Willst du den Status der Seite wirklich in **unsichtbar** umändern?", + "pages.toggle.error.error": "Der Status der Fehlerseite kann nicht geändert werden", + "pages.delete.headline": "Willst du diese Seite wirklich löschen?", + "pages.delete.error.home.headline": "Die Startseite kann nicht gelöscht werden", + "pages.delete.error.home.text": "Du versuchst die Startseite zu löschen. Das ist nicht möglich und würde zu ungewollten Fehlern führen.", + "pages.delete.error.error.headline": "Die Fehlerseite kann nicht gelöscht werden", + "pages.delete.error.error.text": "Du versuchst die Fehlerseite zu löschen. Das ist nicht möglich und würde zu ungewollten Fehlern führen.", + "pages.delete.error.children.headline": "Die Seite kann nicht gelöscht werden", + "pages.delete.error.children.text": "Die Seite hat Unterseiten und kann daher nicht gelöscht werden. Bitte entferne zuerst alle Unterseiten.", + "pages.delete.error.blocked.headline": "Die Seite kann nicht gelöscht werden", + "pages.delete.error.blocked.text": "Die Seite ist blockiert und kann daher nicht gelöscht werden.", + "pages.search.help": "Durchsuche alle Seiten nach URL-Pfad. Du kannst dich durch Ergebnisse mit den Pfeiltasten bewegen und per Enter zur ausgewählten Seite springen.", + "pages.search.noresults": "Es gibt leider keine Seiten zu deiner Suche. Bitte versuche es mit einem anderen Pfad.", + "pages.error.missing": "Die Seite konnte nicht gefunden werden", + "subpages": "Seiten", + "subpages.index.headline": "Seiten in", + "subpages.index.back": "Zurück", + "subpages.index.add": "Neue Seite anlegen", + "subpages.index.add.first.text": "Diese Seite hat noch keine Unterseiten", + "subpages.index.add.first.button": "Lege die erste Seite an", + "subpages.index.visible": "Sichtbare Seiten", + "subpages.index.visible.help": "Ziehe unsichtbare Seiten hierher, um sie zu sortieren/sichtbar zu machen", + "subpages.index.invisible": "Unsichtbare Seiten", + "subpages.index.invisible.help": "Ziehe sichtbare Seiten hierher, um sie unsichtbar zu machen", + "subpages.add.error": "Diese Seite darf keine Unterseiten haben", + "subpages.add.error.more": "Diese Seite kann keine weiteren Unterseiten haben", + "subpages.error.missing": "Die Seite konnte nicht gefunden werden.", + "files": "Dateien", + "files.index.headline": "Dateien für", + "files.index.back": "Zurück", + "files.index.upload": "Neue Datei hochladen", + "files.index.upload.first.text": "Diese Seite hat noch keine Dateien", + "files.index.upload.first.button": "Lade die erste Datei hoch", + "files.index.edit": "Bearbeiten", + "files.index.delete": "Löschen", + "files.index.error.disabled": "Diese Seite darf keine Dateien haben", + "files.add.error.max": "Die maximale Anzahl an Dateien für die aktuelle Seite ist erreicht.", + "files.add.error.extension.missing": "Du kannst keine Dateien ohne Dateiendung hochladen", + "files.add.error.extension.forbidden": "Verbotene Dateiendung", + "files.add.error.mime.forbidden": "Verbotener MIME-Typ", + "files.add.error.htaccess": "htaccess-Dateien können nicht hochgeladen werden", + "files.add.error.invisible": "Versteckte Dateien können nicht hochgeladen werden", + "files.add.blueprint.type.error": "Seite erlaubt nur:", + "files.add.blueprint.size.error": "Seite erlaubt nur eine Dateigröße von", + "files.show.name.label": "Dateiname", + "files.show.info.label": "Typ / Größe / Abmessungen", + "files.show.link.label": "Öffentlicher Link", + "files.show.open": "Anzeigen/Download", + "files.show.back": "Zurück", + "files.show.replace": "Ersetzen", + "files.show.delete": "Löschen", + "files.show.error.rename": "Die Datei konnte nicht umbenannt werden", + "files.show.error.form": "Bitte fülle alle Felder vollständig aus", + "files.upload.drop": "Ziehe Dateien hierher…", + "files.upload.click": "…oder klicke, um Dateien hochzuladen", + "files.replace.drop": "Ziehe eine Datei hierher…", + "files.replace.click": "…oder klicke, um die Datei zu ersetzen", + "files.replace.error.type": "Die hochgeladene Datei muss den selben Dateityp haben", + "files.delete.headline": "Willst du diese Datei wirklich löschen?", + "files.error.missing.page": "Die Seite konnte nicht gefunden werden", + "files.error.missing.file": "Die Datei konnte nicht gefunden werden", + "users": "Benutzer", + "users.index.headline": "Alle Benutzer", + "users.index.add": "Neuen Benutzer anlegen", + "users.index.edit": "Bearbeiten", + "users.index.delete": "Löschen", + "users.form.username.label": "Benutzername", + "users.form.username.placeholder": "Dein Benutzername", + "users.form.username.help": "Format: Kleinbuchstaben a-z, 0-9 und Bindestriche", + "users.form.username.readonly": "Der Benutzername kann nicht geändert werden", + "users.form.firstname.label": "Vorname", + "users.form.lastname.label": "Nachname", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@beispiel.de", + "users.form.password.label": "Passwort", + "users.form.password.confirm.label": "Passwort bestätigen", + "users.form.password.new.label": "Neues Passwort", + "users.form.password.new.confirm.label": "Neues Passwort bestätigen", + "users.form.password.new.help": "Leer lassen, um das aktuelle Passwort zu behalten", + "users.form.language.label": "Sprache", + "users.form.role.label": "Rolle", + "users.form.options.headline": "Accounteinstellungen", + "users.form.options.message": "Email schicken", + "users.form.options.delete": "Account löschen", + "users.form.avatar.headline": "Profilbild", + "users.form.avatar.upload": "Profilbild hochladen", + "users.form.avatar.replace": "Profilbild ersetzen", + "users.form.avatar.delete": "Profilbild löschen", + "users.form.back": "Zurück zur Benutzerübersicht", + "users.form.error.password.confirm": "Bitte bestätige das Passwort", + "users.form.error.update": "Der Benutzer konnte nicht gespeichert werden", + "users.form.error.update.rights": "Du darfst diesen Benutzer nicht aktualisieren", + "users.form.error.create": "Der Benutzer konnte nicht erstellt werden", + "users.form.error.permissions.title": "Der accounts Ordner ist nicht beschreibbar", + "users.form.error.permissions.text": "Bitte stelle sicher, dass /site/accounts besteht und beschreibbar ist.", + "users.delete.headline": "Willst du diesen Benutzer wirklich löschen?", + "users.delete.error": "Der Benutzer konnte nicht gelöscht werden", + "users.delete.error.permission": "Du darfst keine Benutzer löschen", + "users.delete.error.permission.single": "Du darfst diesen Benutzer nicht löschen", + "users.delete.error.lastadmin": "Du kannst den letzten Admin nicht löschen", + "users.avatar.drop": "Ziehe ein Profilbild hierher…", + "users.avatar.click": "…oder klicke, um ein Profilbild hochzuladen", + "users.avatar.error.type": "Es sind nur JPG, PNG und GIF Dateien erlaubt.", + "users.avatar.error.folder.headline": "Der Profilbild Ordner ist nicht beschreibbar", + "users.avatar.error.folder.text": "Bitte erstelle den Ordner /assets/avatars und stelle sicher, dass er beschreibbar ist.", + "users.avatar.error.permission": "Du darfst den Avatar nicht verändern", + "users.avatar.delete.error": "Das Profilbild konnte nicht gelöscht werden", + "users.avatar.delete.error.permission": "Du darfst den Avater dieses Users nicht löschen", + "users.avatar.delete.success": "Das Profilbild wurde gelöscht", + "users.avatar.missing": "Der Benutzer hat kein Profilbild", + "users.error.missing": "Der Benutzer wurde nicht gefunden", + "user.error.lastadmin": "Du bist der letzte Admin. Das kann nicht verändert werden.", + "form.error.missing": "Das Formular kann nicht gefunden werden", + "form.construct.error.invalid": "Ungültiger Formularkonstruktor", + "fields.required": "Pflichtfeld", + "fields.date.label": "Datum", + "fields.date.months": [ + "Januar", + "Februar", + "März", + "April", + "Mai", + "Juni", + "Juli", + "August", + "September", + "Oktober", + "November", + "Dezember" + ], + "fields.date.weekdays": [ + "Sonntag", + "Montag", + "Dienstag", + "Mittwoch", + "Donnerstag", + "Freitag", + "Samstag" + ], + "fields.date.weekdays.short": [ + "So", + "Mo", + "Di", + "Mi", + "Do", + "Fr", + "Sa" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@beispiel.de", + "fields.number.label": "Nummer", + "fields.number.placeholder": "#", + "fields.page.label": "Seite", + "fields.page.placeholder": "pfad/zur/seite", + "fields.password.label": "Passwort", + "fields.structure.add": "Hinzufügen", + "fields.structure.add.first": "Füge den ersten Eintrag hinzu", + "fields.structure.empty": "Es bestehen keine Einträge.", + "fields.structure.entry.error": "Der Eintrag konnte nicht gefunden werden", + "fields.structure.cancel": "Abbrechen", + "fields.structure.save": "Ok", + "fields.structure.edit": "Bearbeiten", + "fields.structure.delete": "Löschen", + "fields.structure.delete.label": "Willst du diesen Eintrag wirklich löschen?", + "fields.tags.label": "Tags", + "fields.tel.label": "Telefon", + "fields.textarea.buttons.bold.label": "Fetter Text", + "fields.textarea.buttons.bold.text": "Fetter Text", + "fields.textarea.buttons.italic.label": "Kursiver Text", + "fields.textarea.buttons.italic.text": "Kursiver Text", + "fields.textarea.buttons.link.label": "Link", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Bild", + "fields.textarea.buttons.file.label": "Datei", + "fields.toggle.yes": "Ja", + "fields.toggle.no": "Nein", + "fields.toggle.on": "An", + "fields.toggle.off": "Aus", + "fields.error.missing.controller": "Die Feldcontroller-Datei fehlt", + "fields.error.missing.class": "Die Feldcontroller-Klasse fehlt", + "fields.error.route.invalid": "Ungültige Feld-Route", + "fields.error.extended": "Das Feld kann nicht erweitert werden", + "editor.link.url.label": "URL einfügen", + "editor.link.text.label": "Linktext", + "editor.link.text.help": "Der Linktext ist optional", + "editor.email.address.label": "Email Adresse einfügen", + "editor.email.address.placeholder": "mail@beispiel.de", + "editor.email.text.label": "Linktext", + "editor.email.text.help": "Der Linktext ist optional", + "editor.file.empty": "Diese Seite hat keine Dateien", + "editor.image.empty": "Diese Seite hat keine Bilder", + "autocomplete.method.error": "Ungültige Autocomplete-Methode", + "blueprints.error.default.missing": "Standard-Blueprint fehlt", + "error": "Fehler", + "error.headline": "Fehler" +} \ No newline at end of file diff --git a/panel/app/translations/de/package.json b/panel/app/translations/de/package.json new file mode 100644 index 0000000..dd2707c --- /dev/null +++ b/panel/app/translations/de/package.json @@ -0,0 +1,4 @@ +{ + "title": "Deutsch", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/en/core.json b/panel/app/translations/en/core.json new file mode 100644 index 0000000..23e5414 --- /dev/null +++ b/panel/app/translations/en/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Cancel", + "add": "Add", + "addit": "Add & Edit", + "save": "Save", + "saved": "Saved!", + "change": "Change", + "delete": "Delete", + "insert": "Insert", + "ok": "Ok", + "routes.error.invalid": "Invalid Panel URL", + "controller.error.invalid": "Invalid controller", + "controller.error.action": "Invalid action", + "view.error.invalid": "Invalid view:", + "options.show": "Show options", + "options.hide": "Hide options", + "installation": "Installation", + "installation.check.headline": "Kirby Panel Installation", + "installation.check.text": "Kirby encountered the following issues during installation…", + "installation.check.retry": "Retry", + "installation.check.error": "There are some issues!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts is not writable", + "installation.check.error.avatars": "/assets/avatars is not writable", + "installation.check.error.blueprints": "Please add a /site/blueprints folder", + "installation.check.error.content": "The content folder and all contained files and folders must be writable.", + "installation.check.error.thumbs": "The thumbs folder must be writable.", + "installation.signup.username.label": "Create your first account", + "installation.signup.username.placeholder": "Username", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@example.com", + "installation.signup.password.label": "Password", + "installation.signup.language.label": "Language", + "installation.signup.button": "Create your account", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Username", + "login.password.label": "Password", + "login.error": "Invalid username or password", + "login.button": "Log in", + "login.log.error.permissions": "Login log file is not writable.", + "logout": "Log out", + "topbar.error.class.definition": "Missing topbar definition for class:", + "dashboard": "Dashboard", + "dashboard.index.pages.title": "Pages", + "dashboard.index.pages.edit": "Edit", + "dashboard.index.pages.add": "Add", + "dashboard.index.site.title": "Your site's URL", + "dashboard.index.account.title": "Your account", + "dashboard.index.account.edit": "Edit", + "dashboard.index.metatags.title": "Site options", + "dashboard.index.metatags.edit": "Edit", + "dashboard.index.history.title": "Your last updates", + "dashboard.index.history.text": "Your last modified pages will be displayed here to make it easy to find them again later.", + "dashboard.index.license.title": "Kirby license", + "dashboard.index.license.text": "It seems you are running Kirby on a public server without a valid license!\n\nPlease, support Kirby and (link: {buy} text: buy a license now)\n\nIf you already have a license key, just add it to your config file: (link: {docs} text: site/config/config.php)", + "metatags": "Site options", + "metatags.info": "Kirby info", + "metatags.license": "Kirby license", + "metatags.version.toolkit": "Toolkit version", + "metatags.version.kirby": "Kirby version", + "metatags.version.panel": "Panel version", + "metatags.back": "Back to the dashboard", + "metatags.files": "Site files", + "site.delete.error": "The site cannot be deleted", + "pages.show.settings": "Page settings", + "pages.show.preview": "Open preview", + "pages.show.template": "Template", + "pages.show.changeurl": "Change URL", + "pages.show.invisible": "Status: invisible", + "pages.show.visible": "Status: visible", + "pages.show.changes.text": "You have unsaved changes!", + "pages.show.changes.button": "Discard", + "pages.show.delete": "Delete this page", + "pages.show.subpages.title": "Pages", + "pages.show.subpages.edit": "Edit", + "pages.show.subpages.add": "Add", + "pages.show.subpages.empty": "This page has no subpages", + "pages.show.files.title": "Files", + "pages.show.files.edit": "Edit", + "pages.show.files.add": "Add", + "pages.show.files.empty": "This page has no files", + "pages.show.error.permissions.title": "The page is not writable", + "pages.show.error.permissions.text": "Please check the permissions for the content folder and all files.", + "pages.show.error.permissions.retry": "Retry", + "pages.show.error.notitle.title": "The blueprint does not have a title field", + "pages.show.error.notitle.text": "Please add a title field and try again", + "pages.show.error.notitle.retry": "Retry", + "pages.show.error.form": "Please fill in all fields correctly", + "pages.add.title.label": "Add a new page", + "pages.add.title.placeholder": "Title", + "pages.add.url.label": "URL-appendix", + "pages.add.url.enter": "(enter your title)", + "pages.add.url.close": "Close", + "pages.add.url.help": "Format: lowercase a-z, 0-9 and regular dashes", + "pages.add.template.label": "Template", + "pages.add.error.create": "The page could not be created", + "pages.add.error.title": "The title is missing", + "pages.add.error.template": "The template is missing", + "pages.add.error.max.headline": "No new pages allowed", + "pages.add.error.max.text": "The maximum number of subpages for the current page has been reached.", + "pages.url.uid.label": "URL-appendix", + "pages.url.uid.label.option": "Create from title", + "pages.url.error.exists": "A page with the same appendix already exists", + "pages.url.error.move": "The appendix could not be changed", + "pages.url.error.rights": "You cannot change the URL of this page", + "pages.template.select.label" : "Template", + "pages.template.warning.text" : "The following fields will change, when you switch the template", + "pages.template.warning.removed" : "Removed fields", + "pages.template.warning.replaced" : "Replaced fields", + "pages.template.warning.added" : "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Position", + "pages.toggle.invisible": "invisible", + "pages.toggle.publish": "Do you really want to change the status of this page to **visible?**", + "pages.toggle.hide": "Do you really want to change the status of this page to **invisible?**", + "pages.toggle.error.error": "The status of the error page cannot be changed", + "pages.delete.headline": "Do you really want to delete this page?", + "pages.delete.error.home.headline": "The home page cannot be deleted", + "pages.delete.error.home.text": "You are trying to delete the home page. This is not possible and would lead to unwanted effects.", + "pages.delete.error.error.headline": "The error page cannot be deleted", + "pages.delete.error.error.text": "You are trying to delete the error page. This is not possible and would lead to unwanted effects.", + "pages.delete.error.children.headline": "The page cannot be deleted", + "pages.delete.error.children.text": "This page has subpages and cannot be deleted. Please delete all subpages first.", + "pages.delete.error.blocked.headline": "The page cannot be deleted", + "pages.delete.error.blocked.text": "This page is locked and cannot be deleted.", + "pages.search.help": "Search pages by URL. Navigate through search results with your up and down arrow keys and hit enter to jump to the selected page.", + "pages.search.noresults": "There are no search results for your query. Please try again with a different URL.", + "pages.error.missing": "The page could not be found", + "subpages": "Pages", + "subpages.index.headline": "Pages in", + "subpages.index.back": "Back", + "subpages.index.add": "Add a new page", + "subpages.index.add.first.text": "This page has no subpages yet", + "subpages.index.add.first.button": "Add the first page", + "subpages.index.visible": "Visible pages", + "subpages.index.visible.help": "Drag invisible pages here to sort them/make them visible.", + "subpages.index.invisible": "Invisible pages", + "subpages.index.invisible.help": "Drag visible pages here to unsort them/make them invisible.", + "subpages.add.error": "This page is not allowed to have subpages", + "subpages.add.error.more": "This page cannot have any more subpages", + "subpages.error.missing": "The page could not be found", + "files": "Files", + "files.index.headline": "Files for", + "files.index.back": "Back", + "files.index.upload": "Upload a new file", + "files.index.upload.first.text": "This page has no files yet", + "files.index.upload.first.button": "Upload the first file", + "files.index.edit": "Edit", + "files.index.delete": "Delete", + "files.index.error.disabled": "The page is not allowed to have any files", + "files.add.error.max": "The maximum number of files for the current page has been reached.", + "files.add.error.extension.missing": "You cannot upload files without extension", + "files.add.error.extension.forbidden": "Forbidden file extension", + "files.add.error.mime.forbidden": "Forbidden mime type", + "files.add.error.htaccess": "htaccess files cannot be uploaded", + "files.add.error.invisible": "Invisible files cannot be uploaded", + "files.add.blueprint.type.error": "Page only allows:", + "files.add.blueprint.size.error": "Page only allows file size of", + "files.show.name.label": "Filename", + "files.show.info.label": "Type / Size / Dimensions", + "files.show.link.label": "Public link", + "files.show.open": "Show/download file", + "files.show.back": "Back", + "files.show.replace": "Replace", + "files.show.delete": "Delete", + "files.show.error.rename": "The file could not be renamed", + "files.show.error.form": "Please fill in all fields correctly", + "files.upload.drop": "Drop files here…", + "files.upload.click": "…or click to upload", + "files.replace.drop": "Drop a file here…", + "files.replace.click": "…or click to replace", + "files.replace.error.type": "The uploaded file must have the same file type", + "files.delete.headline": "Do you really want to delete this file?", + "files.error.missing.page": "The page could not be found", + "files.error.missing.file": "The file could not be found", + "users": "Users", + "users.index.headline": "All users", + "users.index.add": "Add a new user", + "users.index.edit": "Edit", + "users.index.delete": "Delete", + "users.form.username.label": "Username", + "users.form.username.placeholder": "Your username", + "users.form.username.help": "Allowed characters: lowercase a-z, 0-9 and dashes", + "users.form.username.readonly": "The username cannot be changed", + "users.form.firstname.label": "First name", + "users.form.lastname.label": "Last name", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@example.com", + "users.form.password.label": "Password", + "users.form.password.confirm.label": "Confirm password", + "users.form.password.new.label": "New password", + "users.form.password.new.confirm.label": "Confirm the new password", + "users.form.password.new.help": "Leave blank to keep the current password", + "users.form.language.label": "Language", + "users.form.role.label": "Role", + "users.form.options.headline": "Account options", + "users.form.options.message": "Send email", + "users.form.options.delete": "Delete account", + "users.form.avatar.headline": "Profile picture", + "users.form.avatar.upload": "Upload profile picture", + "users.form.avatar.replace": "Replace profile picture", + "users.form.avatar.delete": "Delete profile picture", + "users.form.back": "Back to users", + "users.form.error.password.confirm": "Please confirm the password", + "users.form.error.update": "The user could not be updated", + "users.form.error.update.rights": "You are not allowed to update this user", + "users.form.error.create": "The user could not be created", + "users.form.error.permissions.title": "The account folder is not writable", + "users.form.error.permissions.text": "Please make sure that /site/accounts exists and is writable.", + "users.delete.headline": "Do you really want to delete this user?", + "users.delete.error": "The user could not be deleted", + "users.delete.error.permission": "You are not allowed to delete users", + "users.delete.error.permission.single": "You are not allowed to delete this user", + "users.delete.error.lastadmin": "You cannot delete the last admin", + "users.avatar.drop": "Drop a profile picture here…", + "users.avatar.click": "…or click to upload", + "users.avatar.error.type": "You can only upload JPG, PNG and GIF files", + "users.avatar.error.folder.headline": "The avatar folder is not writable", + "users.avatar.error.folder.text": "Please create the folder /assets/avatars and make it writable to upload profile pictures.", + "users.avatar.error.permission": "You are not allowed to change the avatar", + "users.avatar.delete.error": "The profile picture could not be deleted", + "users.avatar.delete.error.permission": "You are not allowed to delete the avatar of this user", + "users.avatar.delete.success": "The profile picture has been deleted", + "users.avatar.missing": "This user has no avatar", + "users.error.missing": "The user could not be found", + "user.error.lastadmin": "You are the only admin. This cannot be changed.", + "form.error.missing": "The form cannot be found", + "form.construct.error.invalid": "Invalid form construction method", + "fields.required": "Required", + "fields.date.label": "Date", + "fields.date.months": [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ], + "fields.date.weekdays": [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" + ], + "fields.date.weekdays.short": [ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@example.com", + "fields.number.label": "Number", + "fields.number.placeholder": "#", + "fields.page.label": "Page", + "fields.page.placeholder": "path/to/page", + "fields.password.label": "Password", + "fields.structure.add": "Add", + "fields.structure.add.first": "Add the first entry", + "fields.structure.empty": "No entries yet.", + "fields.structure.entry.error": "The item could not be found", + "fields.structure.cancel": "Cancel", + "fields.structure.save": "Ok", + "fields.structure.edit": "Edit", + "fields.structure.delete": "Delete", + "fields.structure.delete.label": "Do you really want to delete this entry?", + "fields.tags.label": "Tags", + "fields.tel.label": "Phone", + "fields.textarea.buttons.bold.label": "Bold text", + "fields.textarea.buttons.bold.text": "Bold text", + "fields.textarea.buttons.italic.label": "Italic text", + "fields.textarea.buttons.italic.text": "Italic text", + "fields.textarea.buttons.link.label": "Link", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Image", + "fields.textarea.buttons.file.label": "File", + "fields.toggle.yes": "Yes", + "fields.toggle.no": "No", + "fields.toggle.on": "On", + "fields.toggle.off": "Off", + "fields.error.missing.controller": "The field controller file is missing", + "fields.error.missing.class": "The field controller class is missing", + "fields.error.route.invalid": "Invalid field route", + "fields.error.extended": "The field cannot be extended", + "editor.link.url.label": "Insert URL", + "editor.link.text.label": "Link text", + "editor.link.text.help": "The link text is optional", + "editor.email.address.label": "Insert email address", + "editor.email.address.placeholder": "mail@example.com", + "editor.email.text.label": "Link text", + "editor.email.text.help": "The link text is optional", + "editor.file.empty": "This page has no files", + "editor.image.empty": "This page has no images", + "autocomplete.method.error": "Invalid autocomplete method", + "blueprints.error.default.missing": "Missing default blueprint", + "error": "Error", + "error.headline": "Error" +} diff --git a/panel/app/translations/en/package.json b/panel/app/translations/en/package.json new file mode 100644 index 0000000..fd2592d --- /dev/null +++ b/panel/app/translations/en/package.json @@ -0,0 +1,4 @@ +{ + "title": "English", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/es_419/core.json b/panel/app/translations/es_419/core.json new file mode 100644 index 0000000..e0d11ff --- /dev/null +++ b/panel/app/translations/es_419/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Cancelar", + "add": "Agregar", + "addit": "Agregar y Editar", + "save": "Guardar", + "saved": "¡Guardado!", + "change": "Cambiar", + "delete": "Eliminar", + "insert": "Insertar", + "ok": "OK", + "routes.error.invalid": "URL del Panel no válida", + "controller.error.invalid": "Controlador no válido", + "controller.error.action": "Acción no válida", + "view.error.invalid": "Vista no válida:", + "options.show": "Mostrar opciones", + "options.hide": "Ocultar opciones", + "installation": "Instalación", + "installation.check.headline": "Instalación del Panel de Kirby", + "installation.check.text": "Kirby encontró los siguientes errores durante la instalación…", + "installation.check.retry": "Reintentar", + "installation.check.error": "¡Tenemos problemas!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts no tine permisos de escritura", + "installation.check.error.avatars": "/assets/avatars no tine permisos de escritura", + "installation.check.error.blueprints": "Por favor agrega una carpeta /site/blueprints", + "installation.check.error.content": "La carpeta \"contenido\" y todos sus archivos y subcarpetas deben de tener permiso de escritura.", + "installation.check.error.thumbs": "La carpeta \"thumbs\" debe tener permisos de escritura.", + "installation.signup.username.label": "Crea tu primera cuenta", + "installation.signup.username.placeholder": "Usuario", + "installation.signup.email.label": "Correo electrónico", + "installation.signup.email.placeholder": "correo@ejemplo.com", + "installation.signup.password.label": "Contraseña", + "installation.signup.language.label": "Idioma", + "installation.signup.button": "Crear tu cuenta", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Usuario", + "login.password.label": "Contraseña", + "login.error": "Usuario o Contraseña equivocada", + "login.button": "Log in", + "login.log.error.permissions": "El archivo de registro del inicio de sesión no es modificable", + "logout": "Log out", + "topbar.error.class.definition": "Falta la definición topbar para la clase:", + "dashboard": "Inicio", + "dashboard.index.pages.title": "Páginas", + "dashboard.index.pages.edit": "Editar", + "dashboard.index.pages.add": "Agregar", + "dashboard.index.site.title": "URL de tu sitio", + "dashboard.index.account.title": "Tu cuenta", + "dashboard.index.account.edit": "Editar", + "dashboard.index.metatags.title": "Variables del sitio", + "dashboard.index.metatags.edit": "Editar", + "dashboard.index.history.title": "Actualizaciones recientes", + "dashboard.index.history.text": "Las últimas páginas modificadas serán desplegadas aquí para facilitar su acceso en el futuro.", + "dashboard.index.license.title": "Licencia Kirby", + "dashboard.index.license.text": "¡Parece que estás ejecutando Kirby en un servidor público sin una licencia válida!\n\nPor favor, apoya a Kirby y (link: {buy} text: compra una licencia)\n\nSi ya has adquirido una licencia, sólo agrégala a tu archivo de configuración: (link: {docs} text: site/config/config.php)", + "metatags": "Variables del sitio", + "metatags.info": "Información de Kirby", + "metatags.license": "Licencia Kirby", + "metatags.version.toolkit": "Versión del Toolkit", + "metatags.version.kirby": "Versión de Kirby", + "metatags.version.panel": "Versión del Panel", + "metatags.back": "Regresar al Inicio", + "metatags.files": "Archivos del sitio", + "site.delete.error": "El sitio no puede ser borrado", + "pages.show.settings": "Opciones de Página", + "pages.show.preview": "Abrir previsualización", + "pages.show.template": "Plantilla", + "pages.show.changeurl": "Cambiar URL", + "pages.show.invisible": "Estatus: invisible", + "pages.show.visible": "Estatus: visible", + "pages.show.changes.text": "¡Tienes cambios sin guardar!", + "pages.show.changes.button": "Descartar", + "pages.show.delete": "Eliminar esta página", + "pages.show.subpages.title": "Páginas", + "pages.show.subpages.edit": "Editar", + "pages.show.subpages.add": "Agregar", + "pages.show.subpages.empty": "Esta página no posee subpáginas", + "pages.show.files.title": "Archivos", + "pages.show.files.edit": "Editar", + "pages.show.files.add": "Agregar", + "pages.show.files.empty": "Esta página no contiene archivos", + "pages.show.error.permissions.title": "Esta página no es modificable", + "pages.show.error.permissions.text": "Por favor revisa los permisos para la carpeta de contenido y todos los archivos.", + "pages.show.error.permissions.retry": "Reintentar", + "pages.show.error.notitle.title": "El blueprint no posee un campo de título", + "pages.show.error.notitle.text": "Por favor agrega un campo de título e intenta de nuevo", + "pages.show.error.notitle.retry": "Reintentar", + "pages.show.error.form": "Por favor llena todos los campos correctamente", + "pages.add.title.label": "Agregar nueva página", + "pages.add.title.placeholder": "Título", + "pages.add.url.label": "Apéndice-URL", + "pages.add.url.enter": "(ingresa un título)", + "pages.add.url.close": "Cerrar", + "pages.add.url.help": "Formato: minúsculas a-z, 0-9 y guiones regulares", + "pages.add.template.label": "Plantilla", + "pages.add.error.create": "La página no pudo ser eliminada", + "pages.add.error.title": "Falta un título", + "pages.add.error.template": "Falta una plantilla", + "pages.add.error.max.headline": "No se permiten páginas nuevas", + "pages.add.error.max.text": "El número máximo de subpáginas para la página actual se ha alcanzado.", + "pages.url.uid.label": "Apéndice-URL", + "pages.url.uid.label.option": "Crear a partir del título", + "pages.url.error.exists": "Una página con el mismo apéndice-url ya existe", + "pages.url.error.move": "El apéndice-url no pudo ser cambiado", + "pages.url.error.rights": "Usted no puede cambiar la URL de esta página", + "pages.template.select.label": "Plantilla", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Posición", + "pages.toggle.invisible": "invisible", + "pages.toggle.publish": "¿En realidad deseas cambiar el estatus de esta página a **visible?**", + "pages.toggle.hide": "¿En realidad deseas cambiar el estatus de esta página a **invisible?**", + "pages.toggle.error.error": "El estado de la pagina de error no puede ser cambiado", + "pages.delete.headline": "¿Estás seguro que deseas eliminar esta página?", + "pages.delete.error.home.headline": "La página \"Home\" no puede ser eliminada", + "pages.delete.error.home.text": "Estás intentando eliminar la página \"Home\". Esto no es posible y podría causar efectos indeseados.", + "pages.delete.error.error.headline": "La página \"Error\" no puede ser eliminada", + "pages.delete.error.error.text": "Estás intentando eliminar la página \"Error\". Esto no es posible y podría causar efectos indeseados.", + "pages.delete.error.children.headline": "Esta página no puede ser eliminada", + "pages.delete.error.children.text": "La página contiene subpáginas y no puede ser eliminada. Por favor elimina todas las subpáginas primero.", + "pages.delete.error.blocked.headline": "Esta página no puede ser eliminada", + "pages.delete.error.blocked.text": "La página esta bloqueada y no puede ser eliminada.", + "pages.search.help": "Buscar páginas por URL. Navega por los resultados de búsqueda con las flechas arriba y abajo y presiona enter para ir a la página seleccionada.", + "pages.search.noresults": "No hay resultados de búsqueda. Por favor intenta con un URL diferente.", + "pages.error.missing": "La página no fue encontrada", + "subpages": "Páginas", + "subpages.index.headline": "Páginas dentro de", + "subpages.index.back": "Regresar", + "subpages.index.add": "Agregar nueva página", + "subpages.index.add.first.text": "Esta página aún no tiene subpáginas", + "subpages.index.add.first.button": "Agrega la primera página", + "subpages.index.visible": "Páginas visibles", + "subpages.index.visible.help": "Arrastra las páginas invisibles aquí para ordenar y hacerlas visibles.", + "subpages.index.invisible": "Páginas invisibles", + "subpages.index.invisible.help": "Arrastra las páginas visibles aquí para eliminar el numero de orden y hacerlas invisibles.", + "subpages.add.error": "Esta página no permite tener subpáginas", + "subpages.add.error.more": "Esta página ya no puede tener más subpáginas", + "subpages.error.missing": "La página no pudo ser encontrada", + "files": "Archivos", + "files.index.headline": "Archivos de", + "files.index.back": "Regresar", + "files.index.upload": "Subir nuevo archivo", + "files.index.upload.first.text": "Esta página aún no contiene archivos", + "files.index.upload.first.button": "Subir el primer archivo", + "files.index.edit": "Editar", + "files.index.delete": "Eliminar", + "files.index.error.disabled": "Esta página no permite tener archivos", + "files.add.error.max": "El máximo número de archivos para la página actual ya han sido alcanzados", + "files.add.error.extension.missing": "Usted no puede subir archivos sin extensión", + "files.add.error.extension.forbidden": "Extensión de archivo prohibida", + "files.add.error.mime.forbidden": "Tipo mime prohibido", + "files.add.error.htaccess": "Los archivos htaccess no pueden ser subidos", + "files.add.error.invisible": "Los archivos invisibles no pueden ser subidos", + "files.add.blueprint.type.error": "La página únicamente permite:", + "files.add.blueprint.size.error": "La página únicamente permite archivos de tamaño", + "files.show.name.label": "Nombre", + "files.show.info.label": "Tipo / Tamaño / Dimensiones", + "files.show.link.label": "Enlace público", + "files.show.open": "Mostrar/descargar archivo", + "files.show.back": "Regresar", + "files.show.replace": "Reemplazar", + "files.show.delete": "Eliminar", + "files.show.error.rename": "El archivo no pudo ser renombrado", + "files.show.error.form": "Por favor llena todos los campos correctamente", + "files.upload.drop": "Arrastra y suelta los archivos aquí…", + "files.upload.click": "…o haz click para subir", + "files.replace.drop": "Arrastra y suelta un archivo aquí…", + "files.replace.click": "…o haz click para reemplazar", + "files.replace.error.type": "El archivo subido debe ser del mismo tipo", + "files.delete.headline": "¿Estás seguro que deseas eliminar este archivo?", + "files.error.missing.page": "La página no pudo ser encontrada", + "files.error.missing.file": "El archivo no pudo ser encontrado", + "users": "Usuarios", + "users.index.headline": "Todos los usuarios", + "users.index.add": "Agregar un nuevo usuario", + "users.index.edit": "Editar", + "users.index.delete": "Eliminar", + "users.form.username.label": "Usuario", + "users.form.username.placeholder": "Tu nombre de usuario", + "users.form.username.help": "Caracteres permitidos: minúsculas a-z, 0-9 y guiones regulares", + "users.form.username.readonly": "El nombre de usuario no puede ser cambiado", + "users.form.firstname.label": "Nombre", + "users.form.lastname.label": "Apellido", + "users.form.email.label": "Email", + "users.form.email.placeholder": "correo@ejemplo.com", + "users.form.password.label": "Contraseña", + "users.form.password.confirm.label": "Confirmar contraseña", + "users.form.password.new.label": "Nueva contraseña", + "users.form.password.new.confirm.label": "Confirmar la nueva contraseña", + "users.form.password.new.help": "Dejar en blanco para mantener la misma contraseña", + "users.form.language.label": "Idioma", + "users.form.role.label": "Rol", + "users.form.options.headline": "Opciones de cuenta", + "users.form.options.message": "Enviar email", + "users.form.options.delete": "Eliminar cuenta", + "users.form.avatar.headline": "Foto de perfil", + "users.form.avatar.upload": "Subir foto de perfil", + "users.form.avatar.replace": "Reemplazar foto de perfil", + "users.form.avatar.delete": "Eliminar foto de perfil", + "users.form.back": "Regresar a usuarios", + "users.form.error.password.confirm": "Por favor confirma la contraseña", + "users.form.error.update": "El usuario no pudo ser actualizado", + "users.form.error.update.rights": "Usted no tiene permitido actualizar este usuario", + "users.form.error.create": "El usuario no pudo ser creado", + "users.form.error.permissions.title": "La carpeta de la cuenta no es modificable", + "users.form.error.permissions.text": "Por favor asegúrate de que la carpeta \"/site/accounts\" exista y sea modificable.", + "users.delete.headline": "¿Estás seguro que deseas eliminar este usuario?", + "users.delete.error": "El ususario no pudo ser eliminado", + "users.delete.error.permission": "Usted no tiene permitido borrar usuarios", + "users.delete.error.permission.single": "Usted no tiene permitido borrar este usuario", + "users.delete.error.lastadmin": "Usted no puede borrar el último administrador", + "users.avatar.drop": "Arrastra y suelta una imagen aquí…", + "users.avatar.click": "…o haz click para subir", + "users.avatar.error.type": "Sólo se pueden subir archivos con extensión JPG, PNG y GIF", + "users.avatar.error.folder.headline": "La carpeta \"avatar\" no es modificable", + "users.avatar.error.folder.text": "Por favor crea la carpeta /assets/avatars y hazla modificable para subir fotos de perfil.", + "users.avatar.error.permission": "Usted no tiene permitido cambiar el avatar", + "users.avatar.delete.error": "La foto de perfil no pudo ser eliminada", + "users.avatar.delete.error.permission": "Usted no tiene permitido borrar el avatar de este usuario", + "users.avatar.delete.success": "La foto de perfil ha sido eliminada", + "users.avatar.missing": "Este usuario no tiene avatar", + "users.error.missing": "El usuario no pudo ser encontrado", + "user.error.lastadmin": "Usted es el único administrador, Esto no puede ser cambiado", + "form.error.missing": "No se pudo encontrar el formulario", + "form.construct.error.invalid": "El método de construcción del formulario es inválido", + "fields.required": "Requerido", + "fields.date.label": "Fecha", + "fields.date.months": [ + "Enero", + "Febrero", + "Marzo", + "Abril", + "Mayo", + "Junio", + "Julio", + "Agosto", + "Septiembre", + "Octubre", + "Noviembre", + "Diciembre" + ], + "fields.date.weekdays": [ + "Domingo", + "Lunes", + "Martes", + "Miércoles", + "Jueves", + "Viernes", + "Sábado" + ], + "fields.date.weekdays.short": [ + "Dom", + "Lun", + "Mar", + "Mié", + "Jue", + "Vie", + "Sáb" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "correo@ejemplo.com", + "fields.number.label": "Número", + "fields.number.placeholder": "#", + "fields.page.label": "Página", + "fields.page.placeholder": "ruta/a/página", + "fields.password.label": "Contraseña", + "fields.structure.add": "Agregar", + "fields.structure.add.first": "Agregar la primera entrada", + "fields.structure.empty": "Aún no existen entradas.", + "fields.structure.entry.error": "No se pudo encontrar el item", + "fields.structure.cancel": "Cancelar", + "fields.structure.save": "Guardar", + "fields.structure.edit": "Editar", + "fields.structure.delete": "Eliminar", + "fields.structure.delete.label": "¿En realidad desea borrar esta entrada?", + "fields.tags.label": "Etiquetas", + "fields.tel.label": "Teléfono", + "fields.textarea.buttons.bold.label": "Texto en Negrita", + "fields.textarea.buttons.bold.text": "Texto en Negrita", + "fields.textarea.buttons.italic.label": "Texto en Itálicas", + "fields.textarea.buttons.italic.text": "Texto en Itálicas", + "fields.textarea.buttons.link.label": "Enlace", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Imágen", + "fields.textarea.buttons.file.label": "Archivo", + "fields.toggle.yes": "Sí", + "fields.toggle.no": "No", + "fields.toggle.on": "Encendido", + "fields.toggle.off": "Apagado", + "fields.error.missing.controller": "Falta el archivo del controlador del campo ", + "fields.error.missing.class": "Falta la clase del controlador del campo", + "fields.error.route.invalid": "La ruta del campo es inválida", + "fields.error.extended": "El campo no puede ser extendido", + "editor.link.url.label": "Insertar URL", + "editor.link.text.label": "Texto de Enlace", + "editor.link.text.help": "El texto de enlace es opcional", + "editor.email.address.label": "Insertar dirección email", + "editor.email.address.placeholder": "correo@ejemplo.com", + "editor.email.text.label": "Texto de Enlace", + "editor.email.text.help": "El texto de enlace es opcional", + "editor.file.empty": "Esta página no contiene archivos", + "editor.image.empty": "Esta página no contiene imágenes", + "autocomplete.method.error": "Método de autocompletar no válido", + "blueprints.error.default.missing": "Falta el blueprint predeterminado", + "error": "Error", + "error.headline": "Error" +} \ No newline at end of file diff --git a/panel/app/translations/es_419/package.json b/panel/app/translations/es_419/package.json new file mode 100644 index 0000000..5a9f078 --- /dev/null +++ b/panel/app/translations/es_419/package.json @@ -0,0 +1,4 @@ +{ + "title": "Español (América Latina)‎", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/es_ES/core.json b/panel/app/translations/es_ES/core.json new file mode 100644 index 0000000..1019834 --- /dev/null +++ b/panel/app/translations/es_ES/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Cancelar", + "add": "Añadir", + "addit": "Añadir y Editar", + "save": "Guardar", + "saved": "¡Guardado!", + "change": "Cambiar", + "delete": "Eliminar", + "insert": "Insertar", + "ok": "OK", + "routes.error.invalid": "La URL del Panel es inválida", + "controller.error.invalid": "Controlador inválido", + "controller.error.action": "Acción inválida", + "view.error.invalid": "Vista inválida", + "options.show": "Mostrar opciones", + "options.hide": "Ocultar opciones", + "installation": "Instalación", + "installation.check.headline": "Instalación de Kirby Panel", + "installation.check.text": "Kirby encontró los siguientes errores durante la instalación…", + "installation.check.retry": "Reintentar", + "installation.check.error": "¡Hay un problema!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts no tiene permisos de escritura", + "installation.check.error.avatars": "/assets/avatars no tiene permisos de escritura", + "installation.check.error.blueprints": "Por favor, crea la carpeta /site/blueprints", + "installation.check.error.content": "La carpeta de contenido y todos sus archivos y carpetas deben tener permisos de escritura.", + "installation.check.error.thumbs": "La carpeta /thumbs debe tener permisos de escritura.", + "installation.signup.username.label": "Crea tu primera cuenta", + "installation.signup.username.placeholder": "Nombre de usuario", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@ejemplo.com", + "installation.signup.password.label": "Contraseña", + "installation.signup.language.label": "Idioma", + "installation.signup.button": "Crear cuenta", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Nombre de usuario", + "login.password.label": "Contraseña", + "login.error": "Nombre de usuario o contraseña incorrecto", + "login.button": "Log in", + "login.log.error.permissions": "El archivo de logs de inicios de sesión no tiene permisos de escritura", + "logout": "Log out", + "topbar.error.class.definition": "Falta la definición de topbar para la clase:", + "dashboard": "Tablero", + "dashboard.index.pages.title": "Páginas", + "dashboard.index.pages.edit": "Editar", + "dashboard.index.pages.add": "Añadir", + "dashboard.index.site.title": "URL de tu sitio", + "dashboard.index.account.title": "Tu cuenta", + "dashboard.index.account.edit": "Editar", + "dashboard.index.metatags.title": "Variables del sitio", + "dashboard.index.metatags.edit": "Editar", + "dashboard.index.history.title": "Tus últimas actualizaciones", + "dashboard.index.history.text": "Las páginas modificadas recientemente se mostrarán aquí para que puedas encontrarlas fácilmente.", + "dashboard.index.license.title": "Licencia de Kirby", + "dashboard.index.license.text": "It seems you are running Kirby on a public server without a valid license!\n\nPlease, support Kirby and (link: {buy} text: buy a license now)\n\nIf you already have a license key, just add it to your config file: (link: {docs} text: site/config/config.php)", + "metatags": "Variables del sitio", + "metatags.info": "Información de Kirby", + "metatags.license": "Licencia de Kirby", + "metatags.version.toolkit": "Versión del Toolkit", + "metatags.version.kirby": "Versión de Kirby", + "metatags.version.panel": "Versión del Panel", + "metatags.back": "Volver al tablero", + "metatags.files": "Archivos del sitio", + "site.delete.error": "No se puede eliminar el sitio", + "pages.show.settings": "Ajustes de página", + "pages.show.preview": "Abrir previsualización", + "pages.show.template": "Plantilla", + "pages.show.changeurl": "Cambiar URL", + "pages.show.invisible": "Estado: Invisible", + "pages.show.visible": "Estado: Visible", + "pages.show.changes.text": "¡Tienes cambios sin guardar!", + "pages.show.changes.button": "Descartar", + "pages.show.delete": "Eliminar esta página", + "pages.show.subpages.title": "Páginas", + "pages.show.subpages.edit": "Editar", + "pages.show.subpages.add": "Añadir", + "pages.show.subpages.empty": "Esta página no tiene subpáginas", + "pages.show.files.title": "Archivos", + "pages.show.files.edit": "Editar", + "pages.show.files.add": "Añadir", + "pages.show.files.empty": "Esta página no tiene archivos", + "pages.show.error.permissions.title": "Esta página no tiene permisos de escritura", + "pages.show.error.permissions.text": "Por favor, revisa los permisos de la carpeta de contenido y todos sus archivos.", + "pages.show.error.permissions.retry": "Volver a intentar", + "pages.show.error.notitle.title": "El blueprint no tiene un campo título", + "pages.show.error.notitle.text": "Por favor, añade un título e inténtalo de nuevo", + "pages.show.error.notitle.retry": "Volver a intentar", + "pages.show.error.form": "Por favor, rellena todos los campos correctamente", + "pages.add.title.label": "Añadir una nueva página", + "pages.add.title.placeholder": "Título", + "pages.add.url.label": "URL de la página", + "pages.add.url.enter": "(añadir título)", + "pages.add.url.close": "Cerrar", + "pages.add.url.help": "Formato: minúsculas a-z, 0-9 y guiones", + "pages.add.template.label": "Plantilla", + "pages.add.error.create": "No se ha podido crear la página", + "pages.add.error.title": "Falta el título", + "pages.add.error.template": "Falta la plantilla", + "pages.add.error.max.headline": "No se permite añadir nuevas páginas", + "pages.add.error.max.text": "Se ha alcanzado el máximo número de subpáginas para la página actual.", + "pages.url.uid.label": "URL de la página", + "pages.url.uid.label.option": "Crear a partir del título", + "pages.url.error.exists": "Ya existe una página con la misma URL", + "pages.url.error.move": "No se ha podido cambiar la URL", + "pages.url.error.rights": "No se puede cambiar la URL de esta página", + "pages.template.select.label": "Plantilla", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Posición", + "pages.toggle.invisible": "invisible", + "pages.toggle.publish": "¿Realmente quieres eliminar esta cambiar el estado de esta página a **visible?**", + "pages.toggle.hide": "¿Realmente quieres cambiar el estado de esta página a **invisible?**", + "pages.toggle.error.error": "No se puede cambiar el estado de la página de error", + "pages.delete.headline": "¿Realmente quieres eliminar esta página?", + "pages.delete.error.home.headline": "No se puede eliminar la página de inicio", + "pages.delete.error.home.text": "Estás intentando eliminar la página de inicio. Esto no es posible y podría causar efectos indeseados.", + "pages.delete.error.error.headline": "No se puede eliminar la página de error", + "pages.delete.error.error.text": "Estás intentando eliminar la página de error. Esto no es posible y podría causar efectos indeseados.", + "pages.delete.error.children.headline": "No se puede eliminar la página", + "pages.delete.error.children.text": "Esta página tiene subpáginas y no puede ser eliminada. Por favor, elimina primero las subpáginas.", + "pages.delete.error.blocked.headline": "No se puede eliminar la página", + "pages.delete.error.blocked.text": "Esta página está bloqueada y no puede ser eliminada.", + "pages.search.help": "Buscar páginas por URL. Navega a través de los resultados de la búsqueda mediante las flechas del teclado y pulsa Enter para ir a la página seleccionada.", + "pages.search.noresults": "No se han encontrado resultados para tu búsqueda. Por favor, inténtalo de nuevo con una URL distinta.", + "pages.error.missing": "No se ha encontrado la página", + "subpages": "Páginas", + "subpages.index.headline": "Páginas en", + "subpages.index.back": "Atrás", + "subpages.index.add": "Añadir una página nueva", + "subpages.index.add.first.text": "Esta página aún no tiene subpáginas", + "subpages.index.add.first.button": "Añade la primera página", + "subpages.index.visible": "Páginas visibles", + "subpages.index.visible.help": "Arrastra páginas invisibles aquí para ordenarlas/hacerlas visibles.", + "subpages.index.invisible": "Páginas invisibles", + "subpages.index.invisible.help": "Arrastra páginas visibles aquí para desordenarlas/hacerlas invisibles.", + "subpages.add.error": "Esta página no admite subpáginas", + "subpages.add.error.more": "Esta página no admite más sumpáginas", + "subpages.error.missing": "No se ha podido encontrar la página", + "files": "Archivos", + "files.index.headline": "Archivos para", + "files.index.back": "Atrás", + "files.index.upload": "Subir un archivo", + "files.index.upload.first.text": "Esta página no tiene archivos", + "files.index.upload.first.button": "Sube un primer archivo", + "files.index.edit": "Editar", + "files.index.delete": "Eliminar", + "files.index.error.disabled": "La página no admite archivos", + "files.add.error.max": "Se ha alcanzado el número máximo de archivos para esta página", + "files.add.error.extension.missing": "No se pueden subir archivos sin extensión", + "files.add.error.extension.forbidden": "Extensión de archivo prohibida", + "files.add.error.mime.forbidden": "mime type prohibido", + "files.add.error.htaccess": "No se pueden subir archivos htacces", + "files.add.error.invisible": "No se pueden subir archivos invisibles", + "files.add.blueprint.type.error": "La página únicamente admite:", + "files.add.blueprint.size.error": "La página únicamente admite tamaño de archivo de", + "files.show.name.label": "Nombre del archivo", + "files.show.info.label": "Tipo / Tamaño / Dimensiones", + "files.show.link.label": "Enlace público", + "files.show.open": "Mostrar/descargar archivo", + "files.show.back": "Atrás", + "files.show.replace": "Reemplazar", + "files.show.delete": "Eliminar", + "files.show.error.rename": "No se ha podido renombrar el archivo", + "files.show.error.form": "Por favor, rellena todos los campos correctamente", + "files.upload.drop": "Suelta archivos aquí…", + "files.upload.click": "…o haz clic para subir", + "files.replace.drop": "Suelta un archivo aquí……", + "files.replace.click": "…o haz clic para reemplazar", + "files.replace.error.type": "El archivo subido debe ser del mismo tipo", + "files.delete.headline": "¿Realmente quieres eliminar este archivo?", + "files.error.missing.page": "No se ha podido encontrar la página", + "files.error.missing.file": "No se ha podido encontrar el archivo", + "users": "Usuarios", + "users.index.headline": "Todos los usuarios", + "users.index.add": "Añadir usuario", + "users.index.edit": "Editar", + "users.index.delete": "Eliminar", + "users.form.username.label": "Nombre de usuario", + "users.form.username.placeholder": "Tu nombre de usuario", + "users.form.username.help": "Caracteres permitidos: minúsculas a-z, 0-9 y guiones", + "users.form.username.readonly": "No se puede cambiar el nombre de usuario", + "users.form.firstname.label": "Nombre", + "users.form.lastname.label": "Apellido", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@ejemplo.com", + "users.form.password.label": "Contraseña", + "users.form.password.confirm.label": "Confirmar contraseña", + "users.form.password.new.label": "Nueva contraseña", + "users.form.password.new.confirm.label": "Confirmar la nueva contraseña", + "users.form.password.new.help": "Dejar vacío para mantener la contraseña actual", + "users.form.language.label": "Idioma", + "users.form.role.label": "Rol", + "users.form.options.headline": "Opciones de cuenta", + "users.form.options.message": "Enviar email", + "users.form.options.delete": "Eliminar cuenta", + "users.form.avatar.headline": "Foto de perfil", + "users.form.avatar.upload": "Subir una foto de perfil", + "users.form.avatar.replace": "Reemplazar la foto de perfil", + "users.form.avatar.delete": "Eliminar la foto de perfil", + "users.form.back": "Volver a Usuarios", + "users.form.error.password.confirm": "Por favor, confirma tu contraseña", + "users.form.error.update": "No se ha podido actualizar el usuario", + "users.form.error.update.rights": "No dispones de autorización para actualizar este usuario", + "users.form.error.create": "No se ha podido crear el usuario", + "users.form.error.permissions.title": "La carpeta de cuentas no tiene permisos de escritura", + "users.form.error.permissions.text": "Por favor, asegúrate de que /site/accounts existe y tiene permisos de escritura.", + "users.delete.headline": "¿Realmente quieres eliminar este usuario?", + "users.delete.error": "No se ha podido eliminar el usuario", + "users.delete.error.permission": "No dispones de autorización para eliminar usuarios", + "users.delete.error.permission.single": "No dispones de autorización para eliminar este usuario", + "users.delete.error.lastadmin": "No puedes eliminar al último admin", + "users.avatar.drop": "Suelta una foto de perfil aquí…", + "users.avatar.click": "…o haz clic para subir", + "users.avatar.error.type": "Sólo se pueden subir archivos JPG, PNG y GIF", + "users.avatar.error.folder.headline": "La carpeta de fotos de perfil no tiene permisos de escritura", + "users.avatar.error.folder.text": "Por favor, crea la carpeta /assets/avatars y asegúrate de que tiene permisos de escritura para poder subir fotos de perfil.", + "users.avatar.error.permission": "No dispones de autorización para cambiar el avatar", + "users.avatar.delete.error": "No se ha podido eliminar la foto de perfil", + "users.avatar.delete.error.permission": "No dispones de autorización para eliminar el avatar de este usuario", + "users.avatar.delete.success": "Se ha eliminado la foto de perfil", + "users.avatar.missing": "Este usuario no tiene avatar", + "users.error.missing": "Usuario no encontrado", + "user.error.lastadmin": "Eres el único administrador. Esto no se puede cambiar.", + "form.error.missing": "No se ha encontrado el formulario", + "form.construct.error.invalid": "Método de construcción de formulario inválido", + "fields.required": "Obligatorio", + "fields.date.label": "Fecha", + "fields.date.months": [ + "Enero", + "Febrero", + "Marzo", + "Abril", + "Mayo", + "Junio", + "Julio", + "Agosto", + "Septiembre", + "Octubre", + "Noviembre", + "Diciembre" + ], + "fields.date.weekdays": [ + "Domingo", + "Lunes", + "Martes", + "Miércoles", + "Jueves", + "Viernes", + "Sábado" + ], + "fields.date.weekdays.short": [ + "Do", + "Lu", + "Ma", + "Mi", + "Ju", + "Vi", + "Sa" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@ejemplo.com", + "fields.number.label": "Número", + "fields.number.placeholder": "#", + "fields.page.label": "Página", + "fields.page.placeholder": "ruta/a/pagina", + "fields.password.label": "Contraseña", + "fields.structure.add": "Añadir", + "fields.structure.add.first": "Añadir la primera entrada", + "fields.structure.empty": "Aún no hay entradas.", + "fields.structure.entry.error": "No se ha podido encontrar el item", + "fields.structure.cancel": "Cancelar", + "fields.structure.save": "Guardar", + "fields.structure.edit": "Editar", + "fields.structure.delete": "Eliminar", + "fields.structure.delete.label": "¿Realmente quieres eliminar esta entrada?", + "fields.tags.label": "Etiquetas", + "fields.tel.label": "Teléfono", + "fields.textarea.buttons.bold.label": "Negrita", + "fields.textarea.buttons.bold.text": "Negrita", + "fields.textarea.buttons.italic.label": "Cursiva", + "fields.textarea.buttons.italic.text": "Cursiva", + "fields.textarea.buttons.link.label": "Enlace", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Imágen", + "fields.textarea.buttons.file.label": "Archivo", + "fields.toggle.yes": "Sí", + "fields.toggle.no": "No", + "fields.toggle.on": "On", + "fields.toggle.off": "Off", + "fields.error.missing.controller": "Falta el archivo del controlador de campos", + "fields.error.missing.class": "Falta la clase del controlador de campos", + "fields.error.route.invalid": "Ruta del campo inválida", + "fields.error.extended": "El campo no puede ser extendido", + "editor.link.url.label": "Insertar URL", + "editor.link.text.label": "Texto del enlace", + "editor.link.text.help": "El texto del enlace es opcional", + "editor.email.address.label": "Añadir dirección de email", + "editor.email.address.placeholder": "mail@ejemplo.com", + "editor.email.text.label": "Texto del enlace", + "editor.email.text.help": "El texto del enlace es opcional", + "editor.file.empty": "Esta página no tiene archivos", + "editor.image.empty": "Esta página no tiene imágenes", + "autocomplete.method.error": "Método de autocompletado inválido", + "blueprints.error.default.missing": "Falta el blueprint por defecto", + "error": "Error", + "error.headline": "Error" +} \ No newline at end of file diff --git a/panel/app/translations/es_ES/package.json b/panel/app/translations/es_ES/package.json new file mode 100644 index 0000000..16056dd --- /dev/null +++ b/panel/app/translations/es_ES/package.json @@ -0,0 +1,4 @@ +{ + "title": "Español (España)", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/fa/core.json b/panel/app/translations/fa/core.json new file mode 100644 index 0000000..18d24f9 --- /dev/null +++ b/panel/app/translations/fa/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "انصراف", + "add": "افزودن", + "addit": "اضافه کردن و ویرایش", + "save": "ذخیره", + "saved": "ذخیره شد!", + "change": "اصلاح", + "delete": "حذف", + "insert": "درج", + "ok": "تایید", + "routes.error.invalid": "آدرس پنل نامعتبر است", + "controller.error.invalid": "کنترلر نامعتبر است", + "controller.error.action": "اقدام نامعتبر", + "view.error.invalid": "ویو نامعتبر است", + "options.show": "نمایش گزینه ها", + "options.hide": "پنهان کردن گزینه ها", + "installation": "نصب و راه اندازی", + "installation.check.headline": "نصب و راه اندازی کربی پنل", + "installation.check.text": "کربی در هنگام نصب با موارد زیر روبرو شده است…", + "installation.check.retry": "تلاش مجدد", + "installation.check.error": "مشکلی رخ داده است!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts قابل نوشتن نیست", + "installation.check.error.avatars": "/assets/avatars قابل نوشتن نیست", + "installation.check.error.blueprints": "لطفا پوشه /site/blueprints را ایجاد کنید", + "installation.check.error.content": "پوشه content و همه فایل ها و پوشه های موجود باید قابل نوشتن باشد.", + "installation.check.error.thumbs": "پوشه thumbs باید قابل نوشتن باشد.", + "installation.signup.username.label": "ایجاد اولین حساب کاربری", + "installation.signup.username.placeholder": "نام کاربری", + "installation.signup.email.label": "پست الکترونیک", + "installation.signup.email.placeholder": "mail@example.com", + "installation.signup.password.label": "گذرواژه", + "installation.signup.language.label": "زبان", + "installation.signup.button": "ایجاد حساب کاربری", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "نام کاربری", + "login.password.label": "گذرواژه", + "login.error": "نام کاربری یا گذرواژه صحیح نیست", + "login.button": "Log in", + "login.log.error.permissions": "فایل تاریخچه ورود کاربران غیرقابل نوشتن است", + "logout": "Log out", + "topbar.error.class.definition": "topbar برای کلاس تعریف نشده است:", + "dashboard": "پیشخوان", + "dashboard.index.pages.title": "صفحات", + "dashboard.index.pages.edit": "ویرایش", + "dashboard.index.pages.add": "افزودن", + "dashboard.index.site.title": "آدرس وبسایت شما", + "dashboard.index.account.title": "مشخصات کاربر جاری", + "dashboard.index.account.edit": "ویرایش", + "dashboard.index.metatags.title": "تنطیمات سایت", + "dashboard.index.metatags.edit": "ویرایش", + "dashboard.index.history.title": "آخرین بروزرسانی ها", + "dashboard.index.history.text": "آخرین صفحاتی که تغییر داده اید در این مکان جهت سهولت دسترسی نمایش داده خواهد شد.", + "dashboard.index.license.title": "مجوز نرم افزار", + "dashboard.index.license.text": "به نظر می‌رسد در حال اجرای کربی در یک سرور عمومی بدون یک مجوز معتبر هستید!\n\nلطفا پشتیبان کربی باشید و (link: {buy} text: همین حالا یک مجوز خریداری کنید)\n\nاگر در حال حاضر دارای یک کلید مجوز هستید، کافی است آن را به فایل پیکربندی خود اضافه کنید: (link: {docs} text: site/config/config.php)", + "metatags": "تنظیمات سایت", + "metatags.info": "اطلاعات نرم افزار", + "metatags.license": "مجوز", + "metatags.version.toolkit": "نسخه هسته", + "metatags.version.kirby": "نسخه نرم افزار", + "metatags.version.panel": "نسخه پنل مدیریت", + "metatags.back": "بازگشت به پیشخوان", + "metatags.files": "فایل های عمومی", + "site.delete.error": "حذف سایت ممکن نیست", + "pages.show.settings": "تنظیمات صفحه", + "pages.show.preview": "پیش نمایش", + "pages.show.template": "قالب صفحه", + "pages.show.changeurl": "تغییر نشانی اینترنتی صفحه", + "pages.show.invisible": "وضعیت: مخفی", + "pages.show.visible": "وضعیت: قابل مشاهده", + "pages.show.changes.text": "برخی تغییرات ذخیره نشده است!", + "pages.show.changes.button": "انصراف", + "pages.show.delete": "حذف صفحه جاری", + "pages.show.subpages.title": "صفحات", + "pages.show.subpages.edit": "ویرایش", + "pages.show.subpages.add": "افزودن", + "pages.show.subpages.empty": "فاقد صفحه فرعی", + "pages.show.files.title": "فایل ها", + "pages.show.files.edit": "ویرایش", + "pages.show.files.add": "افزودن", + "pages.show.files.empty": "فاقد فایل", + "pages.show.error.permissions.title": "صفحه قابل نوشتن نیست", + "pages.show.error.permissions.text": "لطفا دسترسی پوشه محتوا و تمام فایل ها را بررسی نمایید.", + "pages.show.error.permissions.retry": "تلاش مجدد", + "pages.show.error.notitle.title": "طرح الگو فیلد عنوان ندارد.", + "pages.show.error.notitle.text": "لطفا یک فیلد عنوان اضافه کنید و دوباره سعی کنید", + "pages.show.error.notitle.retry": "تلاش مجدد", + "pages.show.error.form": "لطفا همه فیلدها را به درستی پر نمایید", + "pages.add.title.label": "افزودن صفحه جدید", + "pages.add.title.placeholder": "عنوان", + "pages.add.url.label": "پیوست نشانی اینترنتی", + "pages.add.url.enter": "(عنوان خود را وارد کنید)", + "pages.add.url.close": "بستن", + "pages.add.url.help": "Format: lowercase a-z, 0-9 and regular dashes", + "pages.add.template.label": "قالب صفحه", + "pages.add.error.create": "صفحه ایجاد نشد", + "pages.add.error.title": "عنوان وارد نشده است", + "pages.add.error.template": "فالب وارد نشده است.", + "pages.add.error.max.headline": "ساخت صفحه جدید ممکن نیست", + "pages.add.error.max.text": "حداکثر تعداد زیرصفحه برای صفحه فعلی پر شده است.", + "pages.url.uid.label": "پیوست نشانی اینترنتی", + "pages.url.uid.label.option": "ایجاد از روی عنوان", + "pages.url.error.exists": "یک صفحه با پیوست مشابه در حال حاضر وجود دارد", + "pages.url.error.move": "پیوست تغییر نکرد", + "pages.url.error.rights": "شما امکان تغییر آدرس این صفحه را ندارید", + "pages.template.select.label": "قالب صفحه", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "موقعیت", + "pages.toggle.invisible": "مخفی", + "pages.toggle.publish": "آیااز تغییر وضعیت صفحه به **قابل مشاهده** مطمئن هستید؟", + "pages.toggle.hide": "آیااز تغییر وضعیت صفحه به **مخفی** مطمئن هستید؟", + "pages.toggle.error.error": "وضعیت صفحه خطا قابل تغییر نیست", + "pages.delete.headline": "صفحه جاری حذف شود؟", + "pages.delete.error.home.headline": "صفحه اصلی وب سایت نمی تواند حذف شود", + "pages.delete.error.home.text": "شما در حال تلاش برای حذف صفحه اصلی هستید. این امکان پذیر نمی باشد و به اثرات ناخواسته منجر می شود.", + "pages.delete.error.error.headline": "صفحه خطا نمی تواند حذف شود", + "pages.delete.error.error.text": "شما در حال تلاش برای حذف صفحه خطا هستید. این امکان پذیر نمی باشد و به اثرات ناخواسته منجر می شود.", + "pages.delete.error.children.headline": "حذف صفحه ممکن نیست", + "pages.delete.error.children.text": "این صفحه دارای زیرصفحه است و نمی تواند حذف شود. لطفا ابتدا تمام زیرصفحه های آنرا حذف کنید.", + "pages.delete.error.blocked.headline": "حذف صفحه ممکن نیست", + "pages.delete.error.blocked.text": "این صفحه قفل شده است و نمی تواند حذف شود.", + "pages.search.help": "صفحات را بر اساس نشانی اینترنتی جستجو کنید. با استفاده از کلیدهای جهت دار بالا و پایین ردیف مورد نظر خود را انتخاب و جهت انتقال به صفحه انتخابی کلید Enter را فشار دهید.", + "pages.search.noresults": "هیچ نتیجه ای منطبق بر درخواست شما وجود دارد. لطفا دوباره با یک نشانی اینترنتی متفاوت امتحان کنید.", + "pages.error.missing": "صفحه مورد نظر پیدا نشد.", + "subpages": "صفحات", + "subpages.index.headline": "صفحات فرعی", + "subpages.index.back": "بازگشت", + "subpages.index.add": "افزودن صفحه جدید", + "subpages.index.add.first.text": "این صفحه در حال حاضر هیچ زیرصفحه ای ندارد", + "subpages.index.add.first.button": "افزودن نخستین صفحه", + "subpages.index.visible": "صفحات قابل مشاهده", + "subpages.index.visible.help": "صفحات مخفی را جهت مرتب سازی و آشکارسازی به اینجا بکشید.", + "subpages.index.invisible": "صفحات مخفی", + "subpages.index.invisible.help": "صفحات آشکار را جهت مخفی کردن به اینجا بکشید.", + "subpages.add.error": "این صفحه نمی‌تواند دارای صفحات فرعی باشد", + "subpages.add.error.more": "ایجاد صفحات فرعی بیشتر ممکن نیست", + "subpages.error.missing": "صفحه مورد نظر پیدا نشد.", + "files": "فایل ها", + "files.index.headline": "فایل های مرتبط با", + "files.index.back": "بازگشت", + "files.index.upload": "آپلود فایل جدید", + "files.index.upload.first.text": "این صفحه در حال حاضر فاقد فایل است", + "files.index.upload.first.button": "آپلود نخستین فایل", + "files.index.edit": "ویرایش", + "files.index.delete": "حذف", + "files.index.error.disabled": "این صفحه نمی‌تواند دارای فایل باشد", + "files.add.error.max": "محدودیت حداکثر تعداد فایل برای صفحه فعلی سر رسیده است.", + "files.add.error.extension.missing": "شما نمی‌توانید فایل‌های بدون پسوند را آپلود کنید", + "files.add.error.extension.forbidden": "پسوند فایل غیرمجاز است", + "files.add.error.mime.forbidden": "فرمت فایل غیرمجاز است", + "files.add.error.htaccess": "امکان آپلود فایل htaccess وجود ندارد", + "files.add.error.invisible": "امکان آپلود فایلهای مخفی وجود ندارد", + "files.add.blueprint.type.error": "صفحه تنها اجازه می‌دهد که:", + "files.add.blueprint.size.error": "سایز فایل مجاز برای صفحه", + "files.show.name.label": "نام فایل", + "files.show.info.label": "نوع / حجم / ابعاد", + "files.show.link.label": "لینک عمومی", + "files.show.open": "نمایش/دانلود فایل", + "files.show.back": "بازگشت", + "files.show.replace": "جایگزینی", + "files.show.delete": "حذف", + "files.show.error.rename": "فایل را نمی توان تغییر نام داد", + "files.show.error.form": "لطفا همه فیلدها را به درستی پر نمایید", + "files.upload.drop": "فایل ها را اینجا رها کنید…", + "files.upload.click": "…یا جهت انتخاب فایل کلیک کنید", + "files.replace.drop": "فایل ها را اینجا رها کنید…", + "files.replace.click": "…یا جهت انتخاب فایل کلیک کنید", + "files.replace.error.type": "فایل آپلود شده باید نوع فایل یکسان داشته باشد", + "files.delete.headline": "آیا شما واقعا می خواهید این فایل را حذف کنید؟", + "files.error.missing.page": "صفحه مورد نظر پیدا نشد.", + "files.error.missing.file": "فایل مورد نظر پیدا نشد.", + "users": "کاربران", + "users.index.headline": "همه کاربران", + "users.index.add": "افزودن کاربر جدید", + "users.index.edit": "ویرایش", + "users.index.delete": "حذف", + "users.form.username.label": "نام کاربری", + "users.form.username.placeholder": "نام کاربری شما", + "users.form.username.help": "حروف مجاز: حروف کوچک a-z، اعداد 0-9 و خط تیره -", + "users.form.username.readonly": "نام کاربری را نمیتوان تغییر داد", + "users.form.firstname.label": "نام", + "users.form.lastname.label": "نام خانوادگی", + "users.form.email.label": "پست الکترونیک", + "users.form.email.placeholder": "mail@example.com", + "users.form.password.label": "گذرواژه", + "users.form.password.confirm.label": "تکرار گذرواژه", + "users.form.password.new.label": "گذرواژه جدید", + "users.form.password.new.confirm.label": "تکرار گذرواژه", + "users.form.password.new.help": "برای حفظ گذرواژه فعلی فیلد را خالی رها کنید", + "users.form.language.label": "زبان", + "users.form.role.label": "نقش", + "users.form.options.headline": "گزینه های حساب کاربری", + "users.form.options.message": "ارسال پست الکترونیک", + "users.form.options.delete": "حذف حساب کاربری", + "users.form.avatar.headline": "تصویر پروفایل", + "users.form.avatar.upload": "آپلود تصویر پروفایل", + "users.form.avatar.replace": "جایگزینی تصویر پروفایل", + "users.form.avatar.delete": "حذف نصویر پروفایل", + "users.form.back": "بازگشت به لیست کاربران", + "users.form.error.password.confirm": "لطفا تکرار گذرواژه را وارد نمایید", + "users.form.error.update": "کاربر نمی تواند به روز شود", + "users.form.error.update.rights": "شما اجازه بروزرسانی این کاربر را ندارید", + "users.form.error.create": "کاربر نمی تواند ایجاد شود", + "users.form.error.permissions.title": "پوشه account قابل نوشتن نیست", + "users.form.error.permissions.text": "لطفا مطمئن شوید که /site/accounts موجود و قابل نوشتن است.", + "users.delete.headline": "کاربر جاری حذف شود؟", + "users.delete.error": "کاربر نمی تواند حذف شود", + "users.delete.error.permission": "شما اجازه حذف کاربران را ندارید", + "users.delete.error.permission.single": "شما اجازه حذف این کاربر را ندارید", + "users.delete.error.lastadmin": "حذف آخرین مدیر سیستم ممکن نیست", + "users.avatar.drop": "تصویر پروفایل را اینجا رها کنید…", + "users.avatar.click": "یا برای انتخاب و آپلود کلیک کنید", + "users.avatar.error.type": "شما فقط می توانید فایل های JPG، PNG و GIF آپلود کنید", + "users.avatar.error.folder.headline": "پوشه avatar قایل نوشتن نیست", + "users.avatar.error.folder.text": "جهت آپلود تصویر پروفایل پوشه /assets/avatars را ایجاد و آن را قابل نوشتن کنید.", + "users.avatar.error.permission": "شما اجازه تغییر تصویر پروفایل را ندارید", + "users.avatar.delete.error": "تصویر پروفایل را نمیتوان حذف کرد", + "users.avatar.delete.error.permission": "شما اجازه حذف تصویر پروفایل این کاربر را ندارید", + "users.avatar.delete.success": "تصویر پروفایل حذف شد", + "users.avatar.missing": "فاقد تصویر پروفایل", + "users.error.missing": "کاربر مورد نظر پیدا نشد", + "user.error.lastadmin": "شما تنها کاربر مدیر این سیستم هستید و تغییر آن ممکن نیست", + "form.error.missing": "فرم یافت نشد", + "form.construct.error.invalid": "روال ساخت فرم نامعتبر است", + "fields.required": "اجباری", + "fields.date.label": "تاریخ", + "fields.date.months": [ + "ژانویه", + "فوریه", + "مارس", + "آوریل", + "می", + "ژوئن", + "ژوئیه", + "اوت", + "سپتامبر", + "اکتبر", + "نوامبر", + "دسامبر" + ], + "fields.date.weekdays": [ + "یکشنبه", + "دوشنبه", + "سه شنبه", + "چهارشنبه", + "پنجشنبه", + "جمعه", + "شنبه" + ], + "fields.date.weekdays.short": [ + "یکشنبه", + "دوشنبه", + "سه شنبه", + "چهارشنبه", + "پنجشنبه", + "جمعه", + "شنبه" + ], + "fields.email.label": "پست الکترونیک", + "fields.email.placeholder": "mail@example.com", + "fields.number.label": "عدد", + "fields.number.placeholder": "#", + "fields.page.label": "صفحه", + "fields.page.placeholder": "path/to/page", + "fields.password.label": "گذرواژه", + "fields.structure.add": "افزودن", + "fields.structure.add.first": "افزودن نخستین مورد", + "fields.structure.empty": "موردی وجود ندارد.", + "fields.structure.entry.error": "آیتم یافت نشد", + "fields.structure.cancel": "انصراف", + "fields.structure.save": "تایید", + "fields.structure.edit": "ویرایش", + "fields.structure.delete": "حذف", + "fields.structure.delete.label": "مدخل جاری حذف شود؟", + "fields.tags.label": "برچسب ها", + "fields.tel.label": "تلفن", + "fields.textarea.buttons.bold.label": "متن با حروف درشت", + "fields.textarea.buttons.bold.text": "متن با حروف درشت", + "fields.textarea.buttons.italic.label": "متن اریب", + "fields.textarea.buttons.italic.text": "متن اریب", + "fields.textarea.buttons.link.label": "پیوند", + "fields.textarea.buttons.email.label": "پست الکترونیک", + "fields.textarea.buttons.image.label": "تصویر", + "fields.textarea.buttons.file.label": "فایل", + "fields.toggle.yes": "بله", + "fields.toggle.no": "خیر", + "fields.toggle.on": "روشن", + "fields.toggle.off": "خاموش", + "fields.error.missing.controller": "فایل کنترلر فیلد یافت نشد", + "fields.error.missing.class": "کلاس کنترلر فیلد سافت نشد", + "fields.error.route.invalid": "مسید فیلد نامعتبر است", + "fields.error.extended": "توسعه فیلد ممکن نیست", + "editor.link.url.label": "آدرس پیوند", + "editor.link.text.label": "متن پیوند", + "editor.link.text.help": "متن پیوند اختیاری است", + "editor.email.address.label": "درج نشانی پست الکترونیک", + "editor.email.address.placeholder": "mail@example.com", + "editor.email.text.label": "متن پیوند", + "editor.email.text.help": "متن پیوند اختیاری است", + "editor.file.empty": "این صفحه فاقد فایل است", + "editor.image.empty": "این صفحه فاقد تصویر است", + "autocomplete.method.error": "روال تکمیل خودکار معتبر نیست", + "blueprints.error.default.missing": "طرح الگوی پیش‌فرض یافت نشد", + "error": "خطا", + "error.headline": "خطا" +} \ No newline at end of file diff --git a/panel/app/translations/fa/package.json b/panel/app/translations/fa/package.json new file mode 100644 index 0000000..a80ab8f --- /dev/null +++ b/panel/app/translations/fa/package.json @@ -0,0 +1,4 @@ +{ + "title": "پارسی", + "direction": "rtl" +} \ No newline at end of file diff --git a/panel/app/translations/fi/core.json b/panel/app/translations/fi/core.json new file mode 100644 index 0000000..ddfa940 --- /dev/null +++ b/panel/app/translations/fi/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Peruuta", + "add": "Lisää", + "addit": "Add & Edit", + "save": "Tallenna", + "saved": "Tallennettu!", + "change": "Change", + "delete": "Poista", + "insert": "Lisää", + "ok": "Ok", + "routes.error.invalid": "Invalid Panel URL", + "controller.error.invalid": "Invalid controller", + "controller.error.action": "Invalid action", + "view.error.invalid": "Invalid view:", + "options.show": "Näytä asetukset", + "options.hide": "Piilota asetukset", + "installation": "Asennus", + "installation.check.headline": "Kirby Panel asennus", + "installation.check.text": "Kirby havaitsi seuraavat virheet asennuksen yhteydessä…", + "installation.check.retry": "Yritä uudelleen", + "installation.check.error": "Muutama ongelma löytyi!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts edellyttää kirjoitusoikeudet", + "installation.check.error.avatars": "/assets/avatars edellyttää kirjoitusoikeudet", + "installation.check.error.blueprints": "Ole hyvä ja lisää /site/blueprints kansio", + "installation.check.error.content": "/content ja kaikki sen alla olevat tiedostot ja kansiot edellyttävät kirjoitusoikeudet.", + "installation.check.error.thumbs": "/thumbs edellyttää kirjoitusikeudet.", + "installation.signup.username.label": "Luo ensimmäinen käyttäjä", + "installation.signup.username.placeholder": "Tunnus", + "installation.signup.email.label": "Sähköposti", + "installation.signup.email.placeholder": "nimi@osoite.fi", + "installation.signup.password.label": "Salasana", + "installation.signup.language.label": "Kieli", + "installation.signup.button": "Luo käyttäjä", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Käyttäjätunnus", + "login.password.label": "Salasana", + "login.error": "Tunnus tai salasana on virheellinen", + "login.button": "Log in", + "login.log.error.permissions": "Login log file is not writable.", + "logout": "Log out", + "topbar.error.class.definition": "Missing topbar definition for class:", + "dashboard": "Hallintapaneeli", + "dashboard.index.pages.title": "Sivut", + "dashboard.index.pages.edit": "Muokkaa", + "dashboard.index.pages.add": "Lisää", + "dashboard.index.site.title": "Sivuston URL-osoite", + "dashboard.index.account.title": "Käyttäjätilisi", + "dashboard.index.account.edit": "Muokkaa", + "dashboard.index.metatags.title": "Sivuston asetukset", + "dashboard.index.metatags.edit": "Muokkaa", + "dashboard.index.history.title": "Viimeisimmät päivityksesi", + "dashboard.index.history.text": "Viimeksi muokkaamasi sivut listataan tässä jotta löydät ne jatkossa helpommin.", + "dashboard.index.license.title": "Kirby license", + "dashboard.index.license.text": "It seems you are running Kirby on a public server without a valid license!\n\nPlease, support Kirby and (link: {buy} text: buy a license now)\n\nIf you already have a license key, just add it to your config file: (link: {docs} text: site/config/config.php)", + "metatags": "Sivuston asetukset", + "metatags.info": "Kirby info", + "metatags.license": "Kirby license", + "metatags.version.toolkit": "Toolkit version", + "metatags.version.kirby": "Kirby version", + "metatags.version.panel": "Panel version", + "metatags.back": "Takaisin hallintapaneeliin", + "metatags.files": "Site files", + "site.delete.error": "The site cannot be deleted", + "pages.show.settings": "Sivun asetukset", + "pages.show.preview": "Esikatselu", + "pages.show.template": "Sivupohja", + "pages.show.changeurl": "Vaihda URL-osoite", + "pages.show.invisible": "Status: invisible", + "pages.show.visible": "Status: visible", + "pages.show.changes.text": "You have unsaved changes!", + "pages.show.changes.button": "Discard", + "pages.show.delete": "Poista tämä sivu", + "pages.show.subpages.title": "Sivut", + "pages.show.subpages.edit": "Muokkaa", + "pages.show.subpages.add": "Lisää", + "pages.show.subpages.empty": "Tälle sivulle ei ole lisätty alasivuja", + "pages.show.files.title": "Tiedostot", + "pages.show.files.edit": "Muokkaa", + "pages.show.files.add": "Lisää", + "pages.show.files.empty": "Tälle sivulle ei ole lisätty tiedostoja", + "pages.show.error.permissions.title": "Sivun muokkaamista ei ole sallittu", + "pages.show.error.permissions.text": "Ole hyvä ja tarkista kansion /content ja sen alla olevien tiedostojen kirjoitusoikeudet.", + "pages.show.error.permissions.retry": "Yritä uudelleen", + "pages.show.error.notitle.title": "Tässä pohjassa ei ole nimikenttää", + "pages.show.error.notitle.text": "Ole hyvä ja lisää ensin nimikenttä", + "pages.show.error.notitle.retry": "Yritä uudelleen", + "pages.show.error.form": "Ole hyvä ja täytä kaikki kentät oikein.", + "pages.add.title.label": "Lisää uusi sivu", + "pages.add.title.placeholder": "Nimi", + "pages.add.url.label": "URL-osoite", + "pages.add.url.enter": "(kirjoita osoite)", + "pages.add.url.close": "Sulje", + "pages.add.url.help": "Sallitut merkit: pienet kirjaimet a-z, 0-9 sekä väliviivat", + "pages.add.template.label": "Sivupohja", + "pages.add.error.create": "The page could not be created", + "pages.add.error.title": "Otsikko puuttuu", + "pages.add.error.template": "Sivupohjaa ei ole asetettu", + "pages.add.error.max.headline": "Uusia sivuja ei voi enää luoda", + "pages.add.error.max.text": "Nykyiselle sivulle ei voi luoda enempää alasivuja", + "pages.url.uid.label": "URL-pääte", + "pages.url.uid.label.option": "Luo nimen perusteella", + "pages.url.error.exists": "Olemassaolevalla sivulla on jo sama URL-pääte", + "pages.url.error.move": "Päätettä ei voitu muuttaa", + "pages.url.error.rights": "You cannot change the URL of this page", + "pages.template.select.label": "Sivupohja", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Position", + "pages.toggle.invisible": "invisible", + "pages.toggle.publish": "Do you really want to change the status of this page to **visible?**", + "pages.toggle.hide": "Do you really want to change the status of this page to **invisible?**", + "pages.toggle.error.error": "The status of the error page cannot be changed", + "pages.delete.headline": "Haluatko varmasti poistaa sivun?", + "pages.delete.error.home.headline": "Aloitussivua ei voi poistaa", + "pages.delete.error.home.text": "Sivuston aloitussivun poistaminen ei ole mahdollista.", + "pages.delete.error.error.headline": "Virhesivua ei voi poistaa", + "pages.delete.error.error.text": "Virhesivun poistaminen ei ole mahdollista.", + "pages.delete.error.children.headline": "Sivua ei voida poistaa", + "pages.delete.error.children.text": "Tällä sivulla on alasivuja, jotka pitää poistaa ennen kuin itse sivu voidaan poistaa.", + "pages.delete.error.blocked.headline": "Sivua ei voida poistaa", + "pages.delete.error.blocked.text": "Tämä sivu on suojattu eikä sitä voida poistaa.", + "pages.search.help": "Hae sivuja osoitteen perusteella. Voit selata hakutuloksia nuolinäppäimillä ylös tai alas, ja painamalla ENTER siirryt valitulle sivulle.", + "pages.search.noresults": "Hakutermeillä ei löytynyt tuloksia.", + "pages.error.missing": "Sivua ei löytynyt", + "subpages": "Sivut", + "subpages.index.headline": "Alasivut sivulla", + "subpages.index.back": "Takaisin", + "subpages.index.add": "Lisää uusi sivu", + "subpages.index.add.first.text": "Tälle sivulle ei ole vielä lisätty alasivuja", + "subpages.index.add.first.button": "Lisää alasivu", + "subpages.index.visible": "Näkyvät sivut", + "subpages.index.visible.help": "Raahaa piilotetut sivut tähän jotta voit tehdä niistä näkyviä tai muuttaa niiden järjestystä.", + "subpages.index.invisible": "Piilotetut sivut", + "subpages.index.invisible.help": "Raahaa näkyvät sivut tähän niin voit tehdä niistä piilotettuja.", + "subpages.add.error": "This page is not allowed to have subpages", + "subpages.add.error.more": "This page cannot have any more subpages", + "subpages.error.missing": "Sivua ei löytynyt", + "files": "Tiedostot", + "files.index.headline": "Tiedostot sivulla", + "files.index.back": "Takaisin", + "files.index.upload": "Lisää uusi tiedosto", + "files.index.upload.first.text": "Tälle sivulle ei ole lisätty tiedostoja", + "files.index.upload.first.button": "Lisää tiedosto", + "files.index.edit": "Muokkaa", + "files.index.delete": "Poista", + "files.index.error.disabled": "The page is not allowed to have any files", + "files.add.error.max": "The maximum number of files for the current page has been reached.", + "files.add.error.extension.missing": "You cannot upload files without extension", + "files.add.error.extension.forbidden": "Forbidden file extension", + "files.add.error.mime.forbidden": "Forbidden mime type", + "files.add.error.htaccess": "htaccess files cannot be uploaded", + "files.add.error.invisible": "Invisible files cannot be uploaded", + "files.add.blueprint.type.error": "Page only allows:", + "files.add.blueprint.size.error": "Page only allows file size of", + "files.show.name.label": "Tiedostonimi", + "files.show.info.label": "Tyyppi / Koko / Mitat", + "files.show.link.label": "Julkinen linkki", + "files.show.open": "Näytä/lisää tiedosto", + "files.show.back": "Takaisin", + "files.show.replace": "Korvaa", + "files.show.delete": "Poista", + "files.show.error.rename": "Tiedostoa ei voitu nimetä uudelleen", + "files.show.error.form": "Ole hyvä ja täytä kaikki kentät oikein", + "files.upload.drop": "Raahaa tiedostot tähän…", + "files.upload.click": "…tai valitse tiedosto", + "files.replace.drop": "Raahaa tiedosto tähän…", + "files.replace.click": "…tai valitse uusi tiedosto", + "files.replace.error.type": "Uuden ja korvattavan tiedoston tulee olla samaa tyyppiä", + "files.delete.headline": "Haluatko varmasti poistaa tiedoston?", + "files.error.missing.page": "Sivua ei löytynyt", + "files.error.missing.file": "Tiedostoa ei löytynyt", + "users": "Käyttäjät", + "users.index.headline": "Kaikki käyttäjät", + "users.index.add": "Lisää uusi käyttäjä", + "users.index.edit": "Muokkaa", + "users.index.delete": "Poista", + "users.form.username.label": "Käyttäjätunnus", + "users.form.username.placeholder": "Tunnuksesi", + "users.form.username.help": "Sallitut merkit: pienet kirjaimet a-z, 0-9 sekä väliviivat", + "users.form.username.readonly": "Tunnuksen muuttamista ei ole sallittu", + "users.form.firstname.label": "Etunimi", + "users.form.lastname.label": "Sukunimi", + "users.form.email.label": "Sähköposti", + "users.form.email.placeholder": "nimi@osoite.fi", + "users.form.password.label": "Salasana", + "users.form.password.confirm.label": "Salasana uudelleen", + "users.form.password.new.label": "Uusi salasana", + "users.form.password.new.confirm.label": "Uusi salasana uudelleen", + "users.form.password.new.help": "Jätä tyhjäksi jos et halua vaihtaa salasanaasi", + "users.form.language.label": "Kieli", + "users.form.role.label": "Käyttäjätaso", + "users.form.options.headline": "Tilinhallinta", + "users.form.options.message": "Lähetä sähköposti", + "users.form.options.delete": "Poista tili", + "users.form.avatar.headline": "Profiilikuva", + "users.form.avatar.upload": "Lisää profiilikuva", + "users.form.avatar.replace": "Vaihda profiilikuva", + "users.form.avatar.delete": "Poista profiilikuva", + "users.form.back": "Takaisin käyttäjähallintaan", + "users.form.error.password.confirm": "Varmista salasana", + "users.form.error.update": "Käyttäjätilin päivitys epäonnistui", + "users.form.error.update.rights": "You are not allowed to update this user", + "users.form.error.create": "Käyttäjätilin luominen epäonnistui", + "users.form.error.permissions.title": "Käyttäjähakemistoon ei voitu kirjoittaa", + "users.form.error.permissions.text": "Ole hyvä ja varmista että /site/accounts on olemassa ja tarkista sen kirjoitusoikeudet.", + "users.delete.headline": "Haluatko varmsti poistaa käyttäjän?", + "users.delete.error": "Käyttäjää ei voitu poistaa", + "users.delete.error.permission": "You are not allowed to delete users", + "users.delete.error.permission.single": "You are not allowed to delete this user", + "users.delete.error.lastadmin": "You cannot delete the last admin", + "users.avatar.drop": "Raahaa profiilikuva tähän…", + "users.avatar.click": "…tai valitse tiedosto", + "users.avatar.error.type": "Kuvien tulee olla JPG, PNG tai GIF muodossa", + "users.avatar.error.folder.headline": "Kansio avatar vaatii kirjoitusoikeudet", + "users.avatar.error.folder.text": "Profiilikuvan lisääminen edellyttää /assets/avatars kansion luomista ja kirjoitusoikeuksia.", + "users.avatar.error.permission": "You are not allowed to change the avatar", + "users.avatar.delete.error": "Profiilikuvaa ei voitu poistaa", + "users.avatar.delete.error.permission": "You are not allowed to delete the avatar of this user", + "users.avatar.delete.success": "Profiilikuva poistettiin", + "users.avatar.missing": "This user has no avatar", + "users.error.missing": "Käyttäjää ei löytynyt", + "user.error.lastadmin": "You are the only admin. This cannot be changed.", + "form.error.missing": "The form cannot be found", + "form.construct.error.invalid": "Invalid form construction method", + "fields.required": "Pakollinen", + "fields.date.label": "Päivämäärä", + "fields.date.months": [ + "Tammikuu", + "Helmikuu", + "Maaliskuu", + "Huhtikuu", + "Toukokuu", + "Kesäkuu", + "Heinäkuu", + "Elokuu", + "Syyskuu", + "Lokakuu", + "Marraskuu", + "Joulukuu" + ], + "fields.date.weekdays": [ + "Sunnuntai", + "Maanantai", + "Tiistai", + "Keskiviikko", + "Torstai", + "Perjantai", + "Lauantai" + ], + "fields.date.weekdays.short": [ + "Su", + "Ma", + "Ti", + "Ke", + "To", + "Pe", + "La" + ], + "fields.email.label": "Sähköposti", + "fields.email.placeholder": "nimi@osoite.fi", + "fields.number.label": "Numero", + "fields.number.placeholder": "#", + "fields.page.label": "Sivu", + "fields.page.placeholder": "polku/sivulle", + "fields.password.label": "Salasana", + "fields.structure.add": "Lisää", + "fields.structure.add.first": "Lisää ensimmäinen kirjoitus", + "fields.structure.empty": "Ei kirjoituksia toistaiseksi.", + "fields.structure.entry.error": "The item could not be found", + "fields.structure.cancel": "Peruuta", + "fields.structure.save": "Tallenna", + "fields.structure.edit": "Muokkaa", + "fields.structure.delete": "Poista", + "fields.structure.delete.label": "Do you really want to delete this entry?", + "fields.tags.label": "Avainsanat", + "fields.tel.label": "Puhelin", + "fields.textarea.buttons.bold.label": "Lihavointi", + "fields.textarea.buttons.bold.text": "Lihavointi", + "fields.textarea.buttons.italic.label": "Kursivointi", + "fields.textarea.buttons.italic.text": "Kursivointi", + "fields.textarea.buttons.link.label": "Linkki", + "fields.textarea.buttons.email.label": "Sähköposti", + "fields.textarea.buttons.image.label": "Kuva", + "fields.textarea.buttons.file.label": "Tiedosto", + "fields.toggle.yes": "Kyllä", + "fields.toggle.no": "Ei", + "fields.toggle.on": "Päällä", + "fields.toggle.off": "Pois päältä", + "fields.error.missing.controller": "The field controller file is missing", + "fields.error.missing.class": "The field controller class is missing", + "fields.error.route.invalid": "Invalid field route", + "fields.error.extended": "The field cannot be extended", + "editor.link.url.label": "Lisää URL-osoite", + "editor.link.text.label": "Linkin teksti", + "editor.link.text.help": "Linkin teksti on valinnainen", + "editor.email.address.label": "Lisää sähköpostiosoite", + "editor.email.address.placeholder": "nimi@osoite.fi", + "editor.email.text.label": "Linkin testi", + "editor.email.text.help": "Linkin teksti on valinnainen", + "editor.file.empty": "Sivulle ei ole lisätty tiedostoja", + "editor.image.empty": "Sivulle ei ole lisätty kuvia", + "autocomplete.method.error": "Invalid autocomplete method", + "blueprints.error.default.missing": "Missing default blueprint", + "error": "Virhe", + "error.headline": "Virhe" +} \ No newline at end of file diff --git a/panel/app/translations/fi/package.json b/panel/app/translations/fi/package.json new file mode 100644 index 0000000..609698d --- /dev/null +++ b/panel/app/translations/fi/package.json @@ -0,0 +1,4 @@ +{ + "title": "Suomi", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/fr/core.json b/panel/app/translations/fr/core.json new file mode 100644 index 0000000..75930d1 --- /dev/null +++ b/panel/app/translations/fr/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Annuler", + "add": "Ajouter", + "addit": "Ajouter et éditer", + "save": "Enregistrer", + "saved": "Enregistré !", + "change": "Modifier", + "delete": "Supprimer", + "insert": "Insérer", + "ok": "Ok", + "routes.error.invalid": "URL du Panel incorrecte", + "controller.error.invalid": "Contrôleur incorrect", + "controller.error.action": "Action incorrecte", + "view.error.invalid": "Vue incorrecte : ", + "options.show": "Afficher les options", + "options.hide": "Masquer les options", + "installation": "Installation", + "installation.check.headline": "Installation de Kirby Panel", + "installation.check.text": "Durant l’installation, Kirby a rencontré les problèmes suivants…", + "installation.check.retry": "Essayer de nouveau", + "installation.check.error": "Des problèmes on été rencontrés !", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "Le répertoire “/site/accounts” n’est pas accessible en écriture", + "installation.check.error.avatars": "Le répertoire “/assets/avatars” n’est pas accessible en écriture", + "installation.check.error.blueprints": "Veuillez ajouter un répertoire “/site/blueprints”", + "installation.check.error.content": "Le répertoire “/content”, les sous-répertoires et les fichiers qu’il contient doivent être accessibles en écriture.", + "installation.check.error.thumbs": "Le répertoire “thumbs/” doit être accessible en écriture.", + "installation.signup.username.label": "Créer votre premier compte utilisateur", + "installation.signup.username.placeholder": "Nom d’utilisateur", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@exemple.com", + "installation.signup.password.label": "Mot de passe", + "installation.signup.language.label": "Langue", + "installation.signup.button": "Créer votre compte", + "login": "Connexion", + "login.welcome": "Veuillez vous identifier avec votre nouveau compte", + "login.username.label": "Identifiant", + "login.password.label": "Mot de passe", + "login.error": "Identifiant ou mot de passe incorrect", + "login.button": "Connexion", + "login.log.error.permissions": "Le journal de connexion n’est pas accessible en écriture.", + "logout": "Déconnexion", + "topbar.error.class.definition": "Définition de topbar manquante pour la classe : ", + "dashboard": "Tableau de bord", + "dashboard.index.pages.title": "Pages", + "dashboard.index.pages.edit": "Modifier", + "dashboard.index.pages.add": "Ajouter", + "dashboard.index.site.title": "URL de votre site", + "dashboard.index.account.title": "Votre profil", + "dashboard.index.account.edit": "Modifier", + "dashboard.index.metatags.title": "Paramètres du site", + "dashboard.index.metatags.edit": "Modifier", + "dashboard.index.history.title": "Modifications récentes", + "dashboard.index.history.text": "Vos modifications les plus récentes seront affichées ici afin de les retrouver plus aisément.", + "dashboard.index.license.title": "Licence de Kirby", + "dashboard.index.license.text": "Il semblerait que vous utilisiez Kirby sans licence valide sur un serveur public !\n\nMerci de soutenir Kirby en (link: {buy} text: achetant une licence maintenant)\n\nSi vous avez déjà une licence, ajoutez-là simplement à votre fichier de configuration : (link: {docs} text: site/config/config.php)", + "metatags": "Paramètres du site", + "metatags.info": "Informations de Kirby", + "metatags.license": "License de Kirby", + "metatags.version.toolkit": "Version du Toolkit", + "metatags.version.kirby": "Version de Kirby", + "metatags.version.panel": "Version du Panel", + "metatags.back": "Retour au tableau de bord", + "metatags.files": "Fichiers du site", + "site.delete.error": "Le site ne peut être supprimé", + "pages.show.settings": "Options de la page", + "pages.show.preview": "Prévisualiser", + "pages.show.template": "Modèle de page", + "pages.show.changeurl": "Modifier l’URL", + "pages.show.invisible": "Statut : invisible", + "pages.show.visible": "Statut : visible", + "pages.show.changes.text": "Vous avez des modifications non enregistrées !", + "pages.show.changes.button": "Les supprimer", + "pages.show.delete": "Supprimer cette page", + "pages.show.subpages.title": "Pages", + "pages.show.subpages.edit": "Modifier", + "pages.show.subpages.add": "Ajouter", + "pages.show.subpages.empty": "Cette page n’a aucune page secondaire", + "pages.show.files.title": "Fichiers", + "pages.show.files.edit": "Modifier", + "pages.show.files.add": "Ajouter", + "pages.show.files.empty": "Cette page n’a aucun fichier attaché", + "pages.show.error.permissions.title": "La page n’est pas accessible en écriture", + "pages.show.error.permissions.text": "Merci de vérifier les permissions pour le répertoire “/content” et ses fichiers.", + "pages.show.error.permissions.retry": "Essayer de nouveau", + "pages.show.error.notitle.title": "Ce blueprint n’a pas de champs “title”", + "pages.show.error.notitle.text": "Veuillez ajouter un champs “title” et essayer de nouveau", + "pages.show.error.notitle.retry": "Essayer de nouveau", + "pages.show.error.form": "Merci de remplir correctement l’ensemble des champs", + "pages.add.title.label": "Ajouter une nouvelle page", + "pages.add.title.placeholder": "Titre", + "pages.add.url.label": "Identifiant pour l’URL de la page", + "pages.add.url.enter": "(saisir votre titre)", + "pages.add.url.close": "Fermer", + "pages.add.url.help": "Format : minuscules a-z, chiffres 0-9 et tiret simple", + "pages.add.template.label": "Modèle de page", + "pages.add.error.create": "La page n'a pu être créée", + "pages.add.error.title": "Le titre est manquant", + "pages.add.error.template": "Le template est manquant", + "pages.add.error.max.headline": "La création de nouvelles pages secondaires n’est pas autorisé pour cette page", + "pages.add.error.max.text": "Le nombre maximum de sous-pages a été atteint pour cette page.", + "pages.url.uid.label": "Identifiant de l’URL", + "pages.url.uid.label.option": "Créer à partir du titre", + "pages.url.error.exists": "Une page du même nom existe déjà", + "pages.url.error.move": "L’identifiant de l’URL n’a pu être changé", + "pages.url.error.rights": "Vous ne pouvez pas modifier l’URL de cette page", + "pages.template.select.label": "Modèle de page", + "pages.template.warning.text": "Les champs suivants seront modifiés si vous changez de modèle de page", + "pages.template.warning.removed": "Champs supprimés", + "pages.template.warning.replaced": "Champs remplacés", + "pages.template.warning.added": "Champs ajoutés", + "pages.template.error": "Le modèle de cette page ne peut être changé", + "pages.toggle.position": "Position", + "pages.toggle.invisible": "invisible", + "pages.toggle.publish": "Voulez-vous vraiment modifier le statut de cette page en **visible**?", + "pages.toggle.hide": "Voulez-vous vraiment modifier le statut de cette page en **invisible**?", + "pages.toggle.error.error": "Le statut de la page d'erreur ne peut pas être modifié", + "pages.delete.headline": "Voulez-vous vraiment supprimer définitivement cette page ?", + "pages.delete.error.home.headline": "La page d’accueil ne peut être supprimée", + "pages.delete.error.home.text": "Vous essayez de supprimer la page d’accueil. Ce n’est pas possible car cela entraînerait des effets indésirables.", + "pages.delete.error.error.headline": "La page d’erreur ne peut être supprimée", + "pages.delete.error.error.text": "Vous essayez de supprimer la page d’erreur. Ce n’est pas possible car cela aurait des effets indésirables.", + "pages.delete.error.children.headline": "La page ne peut être supprimée", + "pages.delete.error.children.text": "Cette page contient des pages secondaires. Veuillez supprimer les pages associées au préalable.", + "pages.delete.error.blocked.headline": "La page ne peut être supprimée", + "pages.delete.error.blocked.text": "Cette page est verrouillée et ne peut être supprimée.", + "pages.search.help": "Rechercher des pages par URL. Naviguer entre les résultats avec les flèches « haut » et « bas » du clavier, puis appuyer sur la touche « Entrée » pour aller à la page sélectionnée.", + "pages.search.noresults": "Il n’y a pas de résultat à votre recherche. Veuillez essayer de nouveau avec une URL différente.", + "pages.error.missing": "La page n’a pu être trouvée", + "subpages": "Pages", + "subpages.index.headline": "Pages dans", + "subpages.index.back": "Retour", + "subpages.index.add": "Ajouter une nouvelle page", + "subpages.index.add.first.text": "Cette page n’a pas encore de page secondaire", + "subpages.index.add.first.button": "Ajouter une première page", + "subpages.index.visible": "Pages visibles", + "subpages.index.visible.help": "Glisser une page invisible ici pour la classer/la rendre visible.", + "subpages.index.invisible": "Pages invisibles", + "subpages.index.invisible.help": "Glisser une page ici pour la rendre invisible.", + "subpages.add.error": "Cette page ne peut contenir de sous-page", + "subpages.add.error.more": "Cette page ne peut plus avoir de sous-page supplémentaire", + "subpages.error.missing": "La page n’a pu être trouvée", + "files": "Fichiers", + "files.index.headline": "Fichiers pour la page :", + "files.index.back": "Retour", + "files.index.upload": "Ajouter un fichier", + "files.index.upload.first.text": "Cette page n’a pas encore de fichier attaché", + "files.index.upload.first.button": "Ajouter un premier fichier", + "files.index.edit": "Modifier", + "files.index.delete": "Supprimer", + "files.index.error.disabled": "La page ne peut contenir de page secondaire", + "files.add.error.max": "Le nombre maximum de fichiers de cette page a été atteint.", + "files.add.error.extension.missing": "Vous ne pouvez transférer de fichier sans extension", + "files.add.error.extension.forbidden": "Extension de fichier interdite", + "files.add.error.mime.forbidden": "Ce type MIME est interdit", + "files.add.error.htaccess": "Les fichiers htaccess ne peuvent être transférés", + "files.add.error.invisible": "Les fichiers invisibles ne peuvent être transférés", + "files.add.blueprint.type.error": "La page n’autorise que :", + "files.add.blueprint.size.error": "La page n’autorise qu’un poids de fichier de", + "files.show.name.label": "Nom du fichier", + "files.show.info.label": "Type / Poids / Dimensions", + "files.show.link.label": "Lien public", + "files.show.open": "Afficher/télécharger le fichier", + "files.show.back": "Retour", + "files.show.replace": "Remplacer", + "files.show.delete": "Supprimer", + "files.show.error.rename": "Le fichier n’a pu être renommé", + "files.show.error.form": "Merci de remplir correctement chaque champ du formulaire", + "files.upload.drop": "Glisser un fichier ici…", + "files.upload.click": "…ou cliquer pour le transférer depuis votre ordinateur", + "files.replace.drop": "Glisser un fichier ici…", + "files.replace.click": "…ou cliquer pour le remplacer", + "files.replace.error.type": "Le fichier transféré doit être du même type", + "files.delete.headline": "Voulez-vous vraiment supprimer ce fichier ?", + "files.error.missing.page": "La page n’a pu être trouvée", + "files.error.missing.file": "Le fichier n’a pu être trouvé", + "users": "Utilisateurs", + "users.index.headline": "Tous les utilisateurs", + "users.index.add": "Ajouter un utilisateur", + "users.index.edit": "Modifier", + "users.index.delete": "Supprimer", + "users.form.username.label": "Nom d’utilisateur", + "users.form.username.placeholder": "Votre nom d’utilisateur", + "users.form.username.help": "Caractères autorisés : minuscules a-z, chiffres 0-9 et tirets simple", + "users.form.username.readonly": "Le nom d’utilisateur ne peut être modifié", + "users.form.firstname.label": "Prénom", + "users.form.lastname.label": "Nom", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@exemple.com", + "users.form.password.label": "Mot de passe", + "users.form.password.confirm.label": "Confirmer votre mot de passe", + "users.form.password.new.label": "Nouveau mot de passe", + "users.form.password.new.confirm.label": "Confirmer le nouveau mot de passe", + "users.form.password.new.help": "Laisser vide pour conserver votre mot de passe actuel", + "users.form.language.label": "Langue", + "users.form.role.label": "Rôle", + "users.form.options.headline": "Options du compte", + "users.form.options.message": "Envoyer un email", + "users.form.options.delete": "Supprimer le compte", + "users.form.avatar.headline": "Image du profil", + "users.form.avatar.upload": "Ajouter une image de profil", + "users.form.avatar.replace": "Remplacer l’image du profil", + "users.form.avatar.delete": "Supprimer l’image du profil", + "users.form.back": "Retour au compte", + "users.form.error.password.confirm": "Veuillez confirmer le mot de passe", + "users.form.error.update": "Le compte utilisateur ne peut être mis à jour", + "users.form.error.update.rights": "Vous n’êtes pas autorisé à mettre à jour cet utilisateur", + "users.form.error.create": "Le compte utilisateur n’a pu être créé", + "users.form.error.permissions.title": "Le répertoire des comptes n’est pas accessible en écriture", + "users.form.error.permissions.text": "Vérifiez que le répertoire “/site/accounts” existe et est accessible en écriture.", + "users.delete.headline": "Voulez-vous vraiment supprimer ce compte ?", + "users.delete.error": "Le compte utilisateur n’a pu être supprimé", + "users.delete.error.permission": "Vous n’êtes pas autorisé à supprimer des utilisateurs", + "users.delete.error.permission.single": "Vous n’êtes pas autorisé à supprimer cet utilisateur", + "users.delete.error.lastadmin": "Vous ne pouvez supprimer le dernier “admin”", + "users.avatar.drop": "Glisser ici un fichier image…", + "users.avatar.click": "…ou cliquer pour le transférer depuis votre ordinateur", + "users.avatar.error.type": "Vous pouvez uniquement transférer des fichiers JPG, PNG et GIF", + "users.avatar.error.folder.headline": "Le répertoire des avatars n’est pas accessible en écriture", + "users.avatar.error.folder.text": "Veuillez créer le répertoire /assets/avatars/ et en autoriser l'écriture pour pouvoir charger une image de profil.", + "users.avatar.error.permission": "Vous n’êtes pas autorisé à modifier l’avatar", + "users.avatar.delete.error": "L’image du profil n’a pu être supprimée", + "users.avatar.delete.error.permission": "Vous n’êtes pas autorisé à modifier l’avatar de cet utilisateur", + "users.avatar.delete.success": "L’image du profil a été supprimée", + "users.avatar.missing": "Cet utilisateur n’a pas d'avatar", + "users.error.missing": "Aucun compte utilisateur à ce nom n’a été trouvé", + "user.error.lastadmin": "Vous êtes le seul admin. Ceci ne peut être modifié.", + "form.error.missing": "Le formulaire n'a pu être trouvé", + "form.construct.error.invalid": "Méthode de construction de formulaire incorrecte", + "fields.required": "Requis", + "fields.date.label": "Date", + "fields.date.months": [ + "Janvier", + "Février", + "Mars", + "Avril", + "Mai", + "Juin", + "Juillet", + "Août", + "Septembre", + "Octobre", + "Novembre", + "Décembre" + ], + "fields.date.weekdays": [ + "Dimanche", + "Lundi", + "Mardi", + "Mercredi", + "Jeudi", + "Vendredi", + "Samedi" + ], + "fields.date.weekdays.short": [ + "Dim", + "Lun", + "Mar", + "Mer", + "Jeu", + "Ven", + "Sam" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@exemple.com", + "fields.number.label": "Numéro", + "fields.number.placeholder": "N°", + "fields.page.label": "Page", + "fields.page.placeholder": "chemin/vers/la/page", + "fields.password.label": "Mot de passe", + "fields.structure.add": "Ajouter", + "fields.structure.add.first": "Créer la première entrée", + "fields.structure.empty": "Aucune entrée pour le moment.", + "fields.structure.entry.error": "L’élément n’a pu être trouvé", + "fields.structure.cancel": "Annuler", + "fields.structure.save": "Valider", + "fields.structure.edit": "Modifier", + "fields.structure.delete": "Supprimer", + "fields.structure.delete.label": "Voulez-vous vraiment supprimer cette entrée ?", + "fields.tags.label": "Tags", + "fields.tel.label": "Téléphone", + "fields.textarea.buttons.bold.label": "Gras", + "fields.textarea.buttons.bold.text": "Texte en gras", + "fields.textarea.buttons.italic.label": "Italique", + "fields.textarea.buttons.italic.text": "Texte en italique", + "fields.textarea.buttons.link.label": "Lien", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Image", + "fields.textarea.buttons.file.label": "Fichier", + "fields.toggle.yes": "Oui", + "fields.toggle.no": "Non", + "fields.toggle.on": "Activé", + "fields.toggle.off": "Désactivé", + "fields.error.missing.controller": "Le fichier du contrôleur de champ est manquant", + "fields.error.missing.class": "La classe du contrôleur de champ est manquante", + "fields.error.route.invalid": "Routage du champ incorrect", + "fields.error.extended": "Le champ ne peut être étendu", + "editor.link.url.label": "Insérer une URL", + "editor.link.text.label": "Texte du lien", + "editor.link.text.help": "Le texte du lien est optionnel", + "editor.email.address.label": "Insérer une adresse email", + "editor.email.address.placeholder": "mail@exemple.com", + "editor.email.text.label": "Texte du lien", + "editor.email.text.help": "Le texte du lien est optionnel", + "editor.file.empty": "Cette page n’a aucun fichier attaché", + "editor.image.empty": "Cette page n’a aucune image attachée", + "autocomplete.method.error": "Méthode d’auto-complétion incorrecte", + "blueprints.error.default.missing": "Blueprint par défaut manquant", + "error": "Erreur", + "error.headline": "Erreur" +} \ No newline at end of file diff --git a/panel/app/translations/fr/package.json b/panel/app/translations/fr/package.json new file mode 100644 index 0000000..39ea6ab --- /dev/null +++ b/panel/app/translations/fr/package.json @@ -0,0 +1,4 @@ +{ + "title": "Français", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/hu/core.json b/panel/app/translations/hu/core.json new file mode 100644 index 0000000..5924566 --- /dev/null +++ b/panel/app/translations/hu/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Mégsem", + "add": "Hozzáad", + "addit": "Hozzáad és szerkeszt", + "save": "Mentés", + "saved": "Mentve!", + "change": "Módosítás", + "delete": "Törlés", + "insert": "Beilleszt", + "ok": "Ok", + "routes.error.invalid": "Érvénytelen Panel URL", + "controller.error.invalid": "Érvénytelen vezérlő", + "controller.error.action": "Érvénytelen művelet", + "view.error.invalid": "Érvénytelen nézet:", + "options.show": "Beállítások mutatása", + "options.hide": "Beállítások elrejtése", + "installation": "Telepítés", + "installation.check.headline": "Kirby Panel telepítés", + "installation.check.text": "Kirby az alábbi problémával találkozott…", + "installation.check.retry": "Újból", + "installation.check.error": "Van néhány probléma!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts nem írható", + "installation.check.error.avatars": "/assets/avatars nem írható", + "installation.check.error.blueprints": "Kérlek hozd létre a site/blueprints mappát", + "installation.check.error.content": "A content mappa és az összes tartalmazott mappának és fájlnak írhatónak kell lennie.", + "installation.check.error.thumbs": "A thumbs mappának írhatónak kell lennie.", + "installation.signup.username.label": "Első fiók létrehozása", + "installation.signup.username.placeholder": "Felhasználónév", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@példa.hu", + "installation.signup.password.label": "Jelszó", + "installation.signup.language.label": "Nyelv", + "installation.signup.button": "Fiókod létrehozása", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Felhasználónév", + "login.password.label": "Jelszó", + "login.error": "Érvénytelen felhasználónév vagy jelszó", + "login.button": "Log in", + "login.log.error.permissions": "A bejelentkezési logfájl nem írható", + "logout": "Log out", + "topbar.error.class.definition": "Ez az osztály nincs definiálva: ", + "dashboard": "Vezérlőközpont", + "dashboard.index.pages.title": "Oldalak", + "dashboard.index.pages.edit": "Oldal szerkesztése", + "dashboard.index.pages.add": "Új oldal", + "dashboard.index.site.title": "Honlapod URL-je", + "dashboard.index.account.title": "Fiókod", + "dashboard.index.account.edit": "Szerkesztés", + "dashboard.index.metatags.title": "Metatagok", + "dashboard.index.metatags.edit": "Szerkesztés", + "dashboard.index.history.title": "Legutóbbi frissítések", + "dashboard.index.history.text": "Az általad legutóbb módosított oldalakat találod itt, hogy könnyebben megtaláld újból.", + "dashboard.index.license.title": "Kirby licenc", + "dashboard.index.license.text": "Úgy tűnik a Kirby egy publikus szerveren fut érvényes licensz nélkül.\n\nKérlek támogasd a Kirby-t és (link: {buy} text: vásárold meg a licenszet most)\n\nHa már rendelkezel licensz kulccsal, csak add hozzá a konfig fájlodhoz: (link: {docs} text: site/config/config.php)", + "metatags": "Metatagok", + "metatags.info": "Kirby infó", + "metatags.license": "Kirby licenc", + "metatags.version.toolkit": "Toolkit verzió", + "metatags.version.kirby": "Kirby verzió", + "metatags.version.panel": "Panel verzió", + "metatags.back": "Vissza a vezerlőközpontba", + "metatags.files": "A weboldalhoz tartozó fájlok", + "site.delete.error": "A weboldal nem törölhető", + "pages.show.settings": "Oldal beállítások", + "pages.show.preview": "Előnézet megnyitása", + "pages.show.template": "Sablon", + "pages.show.changeurl": "URL változtatása", + "pages.show.invisible": "Állapot: rejtett", + "pages.show.visible": "Állapot: látható", + "pages.show.changes.text": "Nem mentett változtatások vannak!", + "pages.show.changes.button": "Visszavonás", + "pages.show.delete": "Oldal törlése", + "pages.show.subpages.title": "Aloldalak", + "pages.show.subpages.edit": "Aloldal szerkesztése", + "pages.show.subpages.add": "Új aloldal", + "pages.show.subpages.empty": "Az oldalhoz nem tartoznak aloldalak", + "pages.show.files.title": "Fájlok", + "pages.show.files.edit": "Fájl szerkesztése", + "pages.show.files.add": "Új hozzáadása", + "pages.show.files.empty": "Az oldalhoz nem tartoznak fájlok", + "pages.show.error.permissions.title": "Az oldal nem írható", + "pages.show.error.permissions.text": "Kérlek ellenőrizd a content mappa és a fájlok jogosultságát.", + "pages.show.error.permissions.retry": "Újból", + "pages.show.error.notitle.title": "Ennek a blueprintnek nincs cím mezője", + "pages.show.error.notitle.text": "Töltsd ki a cím mezőt és próbálkozz újból", + "pages.show.error.notitle.retry": "Újból", + "pages.show.error.form": "Kérlek minden mezőt tölts ki", + "pages.add.title.label": "Új aloldal hozzáadása", + "pages.add.title.placeholder": "Cím", + "pages.add.url.label": "URL név", + "pages.add.url.enter": "(add meg a címet)", + "pages.add.url.close": "Bezár", + "pages.add.url.help": "Formátum: kisbetűs a-z, 0-9 és kötőjel", + "pages.add.template.label": "Sablon", + "pages.add.error.create": "Az oldal nem hozható létre", + "pages.add.error.title": "Hiányzik a cím", + "pages.add.error.template": "A sablon hiányzik", + "pages.add.error.max.headline": "Új oldal látrehozása nem engedélyezett", + "pages.add.error.max.text": "Elérted a maximálisan létrehozható aloldalak számát.", + "pages.url.uid.label": "URL név", + "pages.url.uid.label.option": "Létrehozás címből", + "pages.url.error.exists": "Van már egy másik oldal ezzel az URL-lel", + "pages.url.error.move": "Az URL-t nem sikerült megváltoztatni", + "pages.url.error.rights": "Ennek az oldalnak nem tudod megváltoztatni az URL-jét", + "pages.template.select.label": "Sablon", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Elhelyezkedés", + "pages.toggle.invisible": "rejtett", + "pages.toggle.publish": "Biztos megváltoztatod az oldal állapotát erre: **látható**?", + "pages.toggle.hide": "Biztos megváltoztatod az oldal állapotát erre: **rejtett**?", + "pages.toggle.error.error": "A hibaoldal állapota nem módosítható", + "pages.delete.headline": "Biztos vagy benne, hogy törlöd az oldalt?", + "pages.delete.error.home.headline": "A főoldal nem törölhető", + "pages.delete.error.home.text": "Te a főoldalt próbálod törölni. Ez nem lehetséges és beláthatatlan következményei lennének.", + "pages.delete.error.error.headline": "A hiba oldal nem törölhető", + "pages.delete.error.error.text": "Te a hiba oldalt próbálod törölni. Ez nem lehetséges és beláthatatlan következményei lennének.", + "pages.delete.error.children.headline": "Az oldal nem törölhető", + "pages.delete.error.children.text": "Az oldalnak vannak aloldalai és nem törölhető.Előbb töröld az összes aloldalt.", + "pages.delete.error.blocked.headline": "Az oldal nem törölhető", + "pages.delete.error.blocked.text": "Ez az oldal zárolt és nem törölhető.", + "pages.search.help": "Keresés URL szerint.Navigálhatsz a fel és le nyilakkal és az enter-rel ugorhatsz a kiválasztott oldalhoz.", + "pages.search.noresults": "Amire kerestél, nincs találat. Kérlek próbáld meg egy másik URL-lel.", + "pages.error.missing": "Az oldal nem található", + "subpages": "Aloldalak", + "subpages.index.headline": "Aloldalak kezelése ennél:", + "subpages.index.back": "Vissza", + "subpages.index.add": "Új aloldal", + "subpages.index.add.first.text": "Ennek az oldalnak még nincs aloldala", + "subpages.index.add.first.button": "Az első aloldal hozzáadása", + "subpages.index.visible": "Látható aloldalak", + "subpages.index.visible.help": "Fogd meg jobbról az oldalt és rendezd vagy tedd láthatóvá.", + "subpages.index.invisible": "Láthatatlan aloldalak", + "subpages.index.invisible.help": "Fogd meg balról az oldalt és rendezd vagy tedd láthatatlanná.", + "subpages.add.error": "Ennek az oldalnak nem lehetnek aloldalai", + "subpages.add.error.more": "Ennek az oldalnak nem lehet több aloldala", + "subpages.error.missing": "Az oldal nem található", + "files": "Fájlok", + "files.index.headline": "Fájlkezelés ennél:", + "files.index.back": "Vissza", + "files.index.upload": "Új fájl feltöltése", + "files.index.upload.first.text": "Ennél az oldalnál még nincs fájl feltöltve", + "files.index.upload.first.button": "Töltsd fel az első fájlt", + "files.index.edit": "Szerkeszt", + "files.index.delete": "Töröl", + "files.index.error.disabled": "Ehhez az oldalhoz nem tartozhatnak fájlok", + "files.add.error.max": "Ehhez az oldalhoz nem tölthető fel a jelenleginél több fájl", + "files.add.error.extension.missing": "Kiterjesztés nélküli fájl nem tölthető fel", + "files.add.error.extension.forbidden": "Tiltott kiterjesztésű fájl", + "files.add.error.mime.forbidden": "Tiltott mime-típus", + "files.add.error.htaccess": "A htaccess fájlt nem lehet feltölteni", + "files.add.error.invisible": "Láthatatlan fájlok nem tölthetők fel", + "files.add.blueprint.type.error": "Az oldal az alábbiakat engedélyezi:", + "files.add.blueprint.size.error": "Az oldal által engedélyezett legnagyobb fájlméret", + "files.show.name.label": "Fájlnév", + "files.show.info.label": "Típus / fájlméret / méretek", + "files.show.link.label": "Publikus link", + "files.show.open": "Fájl megtekintése/letöltése", + "files.show.back": "Vissza", + "files.show.replace": "Cserél", + "files.show.delete": "Töröl", + "files.show.error.rename": "A fájl nem nevezhető át", + "files.show.error.form": "Kérlek minden mezőt tölts ki", + "files.upload.drop": "Dobd a fájlokat ide…", + "files.upload.click": "…vagy kattints ide a feltöltéshez", + "files.replace.drop": "Dobd a fájlt ide…", + "files.replace.click": "…vagy kattints ide a cseréhez", + "files.replace.error.type": "A feltöltött fájlnak azonos a típusa", + "files.delete.headline": "Biztos törölni akarod ezt a fájlt?", + "files.error.missing.page": "Az oldal nem található", + "files.error.missing.file": "A fájl nem található", + "users": "Felhasználók", + "users.index.headline": "Összes felhasználó", + "users.index.add": "Új felhasználó", + "users.index.edit": "szerkeszt", + "users.index.delete": "töröl", + "users.form.username.label": "Felhasználónév", + "users.form.username.placeholder": "Felhasználóneved", + "users.form.username.help": "Engedélyezett karakterek: kisbetűs a-z, 0-9 és kötőjel", + "users.form.username.readonly": "A felhasználónév nem változtatható meg", + "users.form.firstname.label": "Keresztnév", + "users.form.lastname.label": "Vezetéknév", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@pálda.hu", + "users.form.password.label": "Jelszó", + "users.form.password.confirm.label": "Jelszó megerősítése", + "users.form.password.new.label": "Új jelszó", + "users.form.password.new.confirm.label": "Az új jelszó megerősítése", + "users.form.password.new.help": "Hagyd üresen, ha a jelenlegi jelszót meg akarod tartani", + "users.form.language.label": "Nyelv", + "users.form.role.label": "Role", + "users.form.options.headline": "Fiók beállítások", + "users.form.options.message": "Email küldése", + "users.form.options.delete": "Fiók törlése", + "users.form.avatar.headline": "Profil kép", + "users.form.avatar.upload": "Profil kép feltöltése", + "users.form.avatar.replace": "Profil kép cseréje", + "users.form.avatar.delete": "Profil kép törlése", + "users.form.back": "Vissza a felhasználókhoz", + "users.form.error.password.confirm": "Kérlek erősítsd meg a jelszót", + "users.form.error.update": "A felhasználó nem frissíthető", + "users.form.error.update.rights": "Nincs jogosultságod a felhasználó módosításához", + "users.form.error.create": "A felhasználó nem hozható létre", + "users.form.error.permissions.title": "A account mappa nem írható", + "users.form.error.permissions.text": "Kérlek ellenőrizd, hogy a /site/accounts mappa létezik és írható.", + "users.delete.headline": "Biztos törlöd ezt a felhasználót?", + "users.delete.error": "A felhasználó nem törölhető", + "users.delete.error.permission": "Nincs jogosultságod felhasználók törléséhez", + "users.delete.error.permission.single": "Nincs jogosultságod törölni ezt a felhasználót", + "users.delete.error.lastadmin": "Nem törölheted az egyetlen adminisztrátort", + "users.avatar.drop": "Dobd a profil képet ide…", + "users.avatar.click": "…vagy kattints ide a feltöltéshez", + "users.avatar.error.type": "Csak JPG, PNG és GIF fájl tölthető fel", + "users.avatar.error.folder.headline": "Az avatar mappa nem írható", + "users.avatar.error.folder.text": "Kérlek hozd létre a /assets/avatars mappát és tedd írhatóvá a profil képek feltöltéséhez.", + "users.avatar.error.permission": "Nincs jogosultságod megváltoztatni a profilképet", + "users.avatar.delete.error": "A profil kép nem törölhető", + "users.avatar.delete.error.permission": "Nincs jogosultságod megváloztatni ennek a felhasználónak a profilképét", + "users.avatar.delete.success": "A profil kép törölve", + "users.avatar.missing": "Ennek a felhasználónak nincs profilképe", + "users.error.missing": "A felhasználó nem található", + "user.error.lastadmin": "Te vagy az egyetlen adminisztrátor. Ez a beállítás nem módosítható.", + "form.error.missing": "Az űrlap nem található", + "form.construct.error.invalid": "Érvénytelen űrlapfelépítés", + "fields.required": "Kötelező", + "fields.date.label": "Dátum", + "fields.date.months": [ + "január", + "február", + "március", + "április", + "május", + "június", + "július", + "augusztus", + "szeptember", + "október", + "november", + "december" + ], + "fields.date.weekdays": [ + "vasárnap", + "hétfő", + "kedd", + "szerda", + "csütörtök", + "péntek", + "szombat" + ], + "fields.date.weekdays.short": [ + "va", + "hé", + "ke", + "sze", + "csü", + "pé", + "szo" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@példa.hu", + "fields.number.label": "Szám", + "fields.number.placeholder": "#", + "fields.page.label": "Oldal", + "fields.page.placeholder": "az/oldal/elérése", + "fields.password.label": "Jelszó", + "fields.structure.add": "Új", + "fields.structure.add.first": "Első bejegyzés hozzáadása", + "fields.structure.empty": "Nincs még bejegyzés", + "fields.structure.entry.error": "Az elem nem található", + "fields.structure.cancel": "Mégse", + "fields.structure.save": "Mentés", + "fields.structure.edit": "Szerkeszt", + "fields.structure.delete": "Törlés", + "fields.structure.delete.label": "Biztos törölni szeretnéd ezt a bejegyzést?", + "fields.tags.label": "Címkék", + "fields.tel.label": "Telefon", + "fields.textarea.buttons.bold.label": "Félkövér szöveg", + "fields.textarea.buttons.bold.text": "Félkövér szöveg", + "fields.textarea.buttons.italic.label": "Dölt szöveg", + "fields.textarea.buttons.italic.text": "Dölt szöveg", + "fields.textarea.buttons.link.label": "Link", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Kép", + "fields.textarea.buttons.file.label": "Fájl", + "fields.toggle.yes": "Igen", + "fields.toggle.no": "Nem", + "fields.toggle.on": "Be", + "fields.toggle.off": "Ki", + "fields.error.missing.controller": "A mezőszabályozó fájl nincs meg", + "fields.error.missing.class": "A mezőszabályozó osztály hiányzik", + "fields.error.route.invalid": "A mező elérési útvonala érvénytelen", + "fields.error.extended": "Ez a mező nem terjeszthető ki", + "editor.link.url.label": "URL beillesztése", + "editor.link.text.label": "Link szöveg", + "editor.link.text.help": "A link szövege nem kötelező", + "editor.email.address.label": "Email cím beillesztése", + "editor.email.address.placeholder": "mail@példa.hu", + "editor.email.text.label": "Link szöveg", + "editor.email.text.help": "A link szövege nem kötelező", + "editor.file.empty": "Az oldalnak nincsenek fájljai", + "editor.image.empty": "Az oldalnak nincsenek képei", + "autocomplete.method.error": "Érvénytelen auto-kiegészítés metódus", + "blueprints.error.default.missing": "Hiányzó alapértelmezett blueprint", + "error": "Hiba", + "error.headline": "Hiba" +} \ No newline at end of file diff --git a/panel/app/translations/hu/package.json b/panel/app/translations/hu/package.json new file mode 100644 index 0000000..d99d05e --- /dev/null +++ b/panel/app/translations/hu/package.json @@ -0,0 +1,4 @@ +{ + "title": "Hungarian", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/id/core.json b/panel/app/translations/id/core.json new file mode 100644 index 0000000..1a87bc5 --- /dev/null +++ b/panel/app/translations/id/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Batal", + "add": "Tambah", + "addit": "Tambah & Sunting", + "save": "Simpan", + "saved": "Telah disimpan!", + "change": "Change", + "delete": "Hapus", + "insert": "Sisip", + "ok": "Ok", + "routes.error.invalid": "Invalid Panel URL", + "controller.error.invalid": "Invalid controller", + "controller.error.action": "Invalid action", + "view.error.invalid": "Invalid view:", + "options.show": "Tunjukan pilihan", + "options.hide": "Sembunyikan pilihan", + "installation": "Instalasi", + "installation.check.headline": "Instalasi Kirby Panel", + "installation.check.text": "Kirby menemukan beberapa masalah selagi menginstalasi…", + "installation.check.retry": "Coba lagi", + "installation.check.error": "Ada beberapa masalah!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts tidak bisa diakses", + "installation.check.error.avatars": "/assets/avatars tidak bisa diakses", + "installation.check.error.blueprints": "Tolong tambahkan folder /site/blueprints", + "installation.check.error.content": "The content folder and all contained files and folders must be writable.", + "installation.check.error.thumbs": "The thumbs folder must be writable.", + "installation.signup.username.label": "Buatlah akun pertama anda", + "installation.signup.username.placeholder": "Nama pengguna", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "email@contohemail.com", + "installation.signup.password.label": "Kata sandi", + "installation.signup.language.label": "Bahasa", + "installation.signup.button": "Buatlah akun anda", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Nama pengguna", + "login.password.label": "Kata sandi", + "login.error": "Kata sandi atau username anda tidak terdaftar", + "login.button": "Log in", + "login.log.error.permissions": "Berkas log masuk tidak bisa disunting", + "logout": "Log out", + "topbar.error.class.definition": "Missing topbar definition for class:", + "dashboard": "Dashboard", + "dashboard.index.pages.title": "Halaman", + "dashboard.index.pages.edit": "Sunting", + "dashboard.index.pages.add": "Tambah", + "dashboard.index.site.title": "URL situs anda", + "dashboard.index.account.title": "Akun anda", + "dashboard.index.account.edit": "Sunting", + "dashboard.index.metatags.title": "Variabel situs", + "dashboard.index.metatags.edit": "Sunting", + "dashboard.index.history.title": "Update terakhir anda", + "dashboard.index.history.text": "Halaman terakhir yang anda ubah akan ditampilkan di sini supaya mudah untuk menemukannya lagi.", + "dashboard.index.license.title": "Lisensi Kirby", + "dashboard.index.license.text": "It seems you are running Kirby on a public server without a valid license!\n\nPlease, support Kirby and (link: {buy} text: buy a license now)\n\nIf you already have a license key, just add it to your config file: (link: {docs} text: site/config/config.php)", + "metatags": "Variabel situs", + "metatags.info": "Info Kirby", + "metatags.license": "Lisensi Kirby", + "metatags.version.toolkit": "Versi Toolkit", + "metatags.version.kirby": "Versi Kirby", + "metatags.version.panel": "Versi Panel", + "metatags.back": "Kembali ke dashboard", + "metatags.files": "Berkas situs", + "site.delete.error": "Situs tidak bisa dihapus", + "pages.show.settings": "Pengaturan halaman", + "pages.show.preview": "Buka pratinjau", + "pages.show.template": "Template", + "pages.show.changeurl": "Ganti URL", + "pages.show.invisible": "Status: tak terlihat", + "pages.show.visible": "Status: terlihat", + "pages.show.changes.text": "You have unsaved changes!", + "pages.show.changes.button": "Discard", + "pages.show.delete": "Hapus halaman ini", + "pages.show.subpages.title": "Halaman", + "pages.show.subpages.edit": "Sunting", + "pages.show.subpages.add": "Tambah", + "pages.show.subpages.empty": "Halaman ini tidak memiliki sub-halaman", + "pages.show.files.title": "Berkas", + "pages.show.files.edit": "Sunting", + "pages.show.files.add": "Tambah", + "pages.show.files.empty": "Halaman ini tidak memiliki berkas", + "pages.show.error.permissions.title": "Halaman ini tidak bisa disunting", + "pages.show.error.permissions.text": "Tolong periksa hak akses untuk file dan folder.", + "pages.show.error.permissions.retry": "Coba lagi", + "pages.show.error.notitle.title": "Blueprint ini tidak memiliki bidang untuk 'title'", + "pages.show.error.notitle.text": "Tolong tambahkan bidang untuk 'title' dan coba lagi", + "pages.show.error.notitle.retry": "Coba lagi", + "pages.show.error.form": "Tolong isi semua bidang dengan benar", + "pages.add.title.label": "Tambah halaman baru", + "pages.add.title.placeholder": "Judul", + "pages.add.url.label": "URL-lampiran", + "pages.add.url.enter": "(masukkan judul anda)", + "pages.add.url.close": "Tutup", + "pages.add.url.help": "Format: huruf kecil a-z, 0-9 dan regular dashes", + "pages.add.template.label": "Template", + "pages.add.error.create": "Halaman tidak dapat dibuat", + "pages.add.error.title": "Judul tidak ditemukan", + "pages.add.error.template": "Template tidak ditemukan", + "pages.add.error.max.headline": "Halaman baru tidak diperbolehkan", + "pages.add.error.max.text": "Jumlah maksimum sub-halaman telah tercapai.", + "pages.url.uid.label": "URL-lampiran", + "pages.url.uid.label.option": "Buat dari judul", + "pages.url.error.exists": "Halaman dengan lampiran yang sama sudah ada", + "pages.url.error.move": "Lampiran tidak bisa diubah", + "pages.url.error.rights": "You cannot change the URL of this page", + "pages.template.select.label": "Template", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Posisi", + "pages.toggle.invisible": "Status: tak terlihat", + "pages.toggle.publish": "Apakah anda benar-benar mau mengganti status halaman ini menjadi **visible?**", + "pages.toggle.hide": "Do you really want to change the status of this page to **invisible?**", + "pages.toggle.error.error": "The status of the error page cannot be changed", + "pages.delete.headline": "Apakah anda benar-benar mau menghapus halaman ini?", + "pages.delete.error.home.headline": "Beranda tidak bisa dihapus", + "pages.delete.error.home.text": "Anda sedang mencoba menghapus beranda. Hal ini tidak mungkin dan akan menyebabkan efek yang tidak diinginkan.", + "pages.delete.error.error.headline": "Halaman error tidak bisa dihapus", + "pages.delete.error.error.text": "Anda sedang mencoba menghapus halaman error. Hal ini tidak mungkin dan akan menyebabkan efek yang tidak diinginkan.", + "pages.delete.error.children.headline": "Halaman ini tidak bisa dihapus", + "pages.delete.error.children.text": "Halaman ini memiliki sub-halaman dan tidak bisa dihapus. Tolong hapus sub-halaman terdahulu.", + "pages.delete.error.blocked.headline": "Halaman ini tidak bisa dihapus", + "pages.delete.error.blocked.text": "Halaman ini dikunci dan tidak bisa dihapus.", + "pages.search.help": "Cari halaman dengan URL. Navigasi hasil pencarian dengan tombol atas dan bawah kemudian tekan tombol enter untuk melompat ke halaman yang dipilih.", + "pages.search.noresults": "Tidak ada hasil pencarian untuk permintaan anda. Silakan coba lagi dengan URL yang berbeda.", + "pages.error.missing": "Halaman yang anda cari tidak bisa ditemukan.", + "subpages": "Halaman", + "subpages.index.headline": "Halaman yang ada didalam", + "subpages.index.back": "Kembali", + "subpages.index.add": "Tambah halaman baru", + "subpages.index.add.first.text": "Halaman ini belum memiliki sub-halaman", + "subpages.index.add.first.button": "Tambah halaman pertama", + "subpages.index.visible": "Halaman terlihat", + "subpages.index.visible.help": "Seretlah halaman tersembunyi ke sini untuk menyortir atau membuat halaman terlihat", + "subpages.index.invisible": "Halaman tersembunyi", + "subpages.index.invisible.help": "Seretlah halaman terlihat ke sini untuk menyortir atau membuat halaman tersembunyi.", + "subpages.add.error": "This page is not allowed to have subpages", + "subpages.add.error.more": "This page cannot have any more subpages", + "subpages.error.missing": "Halaman tidak bisa ditemukan", + "files": "Berkas", + "files.index.headline": "Berkas untuk", + "files.index.back": "Kembali", + "files.index.upload": "Unggah berkas baru", + "files.index.upload.first.text": "Halaman ini belum memiliki berkas", + "files.index.upload.first.button": "Unggah berkas pertama", + "files.index.edit": "Sunting", + "files.index.delete": "Hapus", + "files.index.error.disabled": "The page is not allowed to have any files", + "files.add.error.max": "The maximum number of files for the current page has been reached.", + "files.add.error.extension.missing": "You cannot upload files without extension", + "files.add.error.extension.forbidden": "Forbidden file extension", + "files.add.error.mime.forbidden": "Forbidden mime type", + "files.add.error.htaccess": "htaccess files cannot be uploaded", + "files.add.error.invisible": "Invisible files cannot be uploaded", + "files.add.blueprint.type.error": "Page only allows:", + "files.add.blueprint.size.error": "Page only allows file size of", + "files.show.name.label": "Nama berkas", + "files.show.info.label": "Tipe / Ukuran / Dimensi", + "files.show.link.label": "Link publik", + "files.show.open": "Tampilkan atau download file", + "files.show.back": "Kembali", + "files.show.replace": "Ganti", + "files.show.delete": "Hapus", + "files.show.error.rename": "Nama berkas tidak bisa diganti", + "files.show.error.form": "Tolong isi semua bidang dengan benar", + "files.upload.drop": "Drop file disini…", + "files.upload.click": "…atau tekan untuk meng-upload", + "files.replace.drop": "Drop file disini…", + "files.replace.click": "…atau tekan untuk mengganti", + "files.replace.error.type": "Berkas yang diunggah harus memiliki tipe yang sama.", + "files.delete.headline": "Apakah anda benar-benar mau menghapus file ini?", + "files.error.missing.page": "Halaman tidak bisa ditemukan", + "files.error.missing.file": "File tidak bisa ditemukan", + "users": "Pengguna", + "users.index.headline": "Semua pengguna", + "users.index.add": "Tambah pengguna baru", + "users.index.edit": "Sunting", + "users.index.delete": "Hapus", + "users.form.username.label": "Nama pengguna", + "users.form.username.placeholder": "Nama pengguna anda", + "users.form.username.help": "Karakter yang diperbolehkan: huruf kecil a-z, 0-9 dan strip", + "users.form.username.readonly": "Nama pengguna tidak bisa diganti", + "users.form.firstname.label": "Nama depan", + "users.form.lastname.label": "Nama keluarga", + "users.form.email.label": "Email", + "users.form.email.placeholder": "email@contohemail.com", + "users.form.password.label": "Kata sandi", + "users.form.password.confirm.label": "Konfirmasi kata sandi", + "users.form.password.new.label": "Kata sandi baru", + "users.form.password.new.confirm.label": "Konfirmasi kata sandi yang baru", + "users.form.password.new.help": "Biarkan kosong untuk menyimpan kata sandi yang sekarang", + "users.form.language.label": "Bahasa", + "users.form.role.label": "Peran", + "users.form.options.headline": "Opsi akun", + "users.form.options.message": "Kirim email", + "users.form.options.delete": "Hapus akun", + "users.form.avatar.headline": "Gambar profil", + "users.form.avatar.upload": "Unggah gambar profil", + "users.form.avatar.replace": "Ganti gambar profil", + "users.form.avatar.delete": "Hapus gambar profil", + "users.form.back": "Kembali ke pengguna", + "users.form.error.password.confirm": "Tolong konfirmasi kata sandi", + "users.form.error.update": "Pengguna tidak bisa di-update", + "users.form.error.update.rights": "You are not allowed to update this user", + "users.form.error.create": "Pengguna tidak bisa dibuat", + "users.form.error.permissions.title": "Folder 'account' tidak bisa diakses", + "users.form.error.permissions.text": "Tolong pastikan bahwa /site/accounts ada dan bisa diakses.", + "users.delete.headline": "Apakah anda benar-benar mau menghapus pengguna ini?", + "users.delete.error": "Pengguna ini tidak bisa dihapus", + "users.delete.error.permission": "You are not allowed to delete users", + "users.delete.error.permission.single": "You are not allowed to delete this user", + "users.delete.error.lastadmin": "You cannot delete the last admin", + "users.avatar.drop": "Drop profil foto disini…", + "users.avatar.click": "…or atau tekan untuk meng-upload", + "users.avatar.error.type": "Anda hanya bisa meng-upload file dengan tipe JPG, PNG dan GIF", + "users.avatar.error.folder.headline": "Folder avatar tidak bisa diakses", + "users.avatar.error.folder.text": "Tolong buat folder /assets/avatars dan berikan hak akses untuk mengupload profil foto.", + "users.avatar.error.permission": "You are not allowed to change the avatar", + "users.avatar.delete.error": "Profil fotonya tidak bisa dihapus", + "users.avatar.delete.error.permission": "You are not allowed to delete the avatar of this user", + "users.avatar.delete.success": "Profil fotonya telah dihapus", + "users.avatar.missing": "This user has no avatar", + "users.error.missing": "Pengguna tidak bisa ditemukan", + "user.error.lastadmin": "You are the only admin. This cannot be changed.", + "form.error.missing": "The form cannot be found", + "form.construct.error.invalid": "Invalid form construction method", + "fields.required": "Diperlukan", + "fields.date.label": "Tanggal", + "fields.date.months": [ + "Januari", + "Februari", + "Maret", + "April", + "Mei", + "Juni", + "Juli", + "Agustus", + "September", + "Oktober", + "November", + "Desember" + ], + "fields.date.weekdays": [ + "Minggu", + "Senin", + "Selasa", + "Rabu", + "Kamis", + "Jum'at", + "Sabtu" + ], + "fields.date.weekdays.short": [ + "Min", + "Sen", + "Sel", + "Rab", + "Kam", + "Jum", + "Sab" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@example.com", + "fields.number.label": "Nomor", + "fields.number.placeholder": "#", + "fields.page.label": "Halaman", + "fields.page.placeholder": "path/ke/halaman", + "fields.password.label": "Kata sandi", + "fields.structure.add": "Tambah", + "fields.structure.add.first": "Tambahkan entri pertama", + "fields.structure.empty": "Belum ada entri", + "fields.structure.entry.error": "The item could not be found", + "fields.structure.cancel": "Batal", + "fields.structure.save": "Simpan", + "fields.structure.edit": "Sunting", + "fields.structure.delete": "Hapus", + "fields.structure.delete.label": "Do you really want to delete this entry?", + "fields.tags.label": "Tags", + "fields.tel.label": "Telepon", + "fields.textarea.buttons.bold.label": "Teks tebal", + "fields.textarea.buttons.bold.text": "Teks tebal", + "fields.textarea.buttons.italic.label": "Teks miring", + "fields.textarea.buttons.italic.text": "Teks miring", + "fields.textarea.buttons.link.label": "Link", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Gambar", + "fields.textarea.buttons.file.label": "Berkas", + "fields.toggle.yes": "Ya", + "fields.toggle.no": "Tidak", + "fields.toggle.on": "On", + "fields.toggle.off": "Off", + "fields.error.missing.controller": "The field controller file is missing", + "fields.error.missing.class": "The field controller class is missing", + "fields.error.route.invalid": "Invalid field route", + "fields.error.extended": "The field cannot be extended", + "editor.link.url.label": "Masukkan URL", + "editor.link.text.label": "Link text", + "editor.link.text.help": "Link text opsional", + "editor.email.address.label": "Masukkan alamat email", + "editor.email.address.placeholder": "email@contohemail.com", + "editor.email.text.label": "Link text", + "editor.email.text.help": "Link text opsional", + "editor.file.empty": "Halaman ini tidak memiliki file", + "editor.image.empty": "Halaman ini tidak memiliki gambar", + "autocomplete.method.error": "Invalid autocomplete method", + "blueprints.error.default.missing": "Missing default blueprint", + "error": "Error", + "error.headline": "Error" +} \ No newline at end of file diff --git a/panel/app/translations/id/package.json b/panel/app/translations/id/package.json new file mode 100644 index 0000000..967ccfc --- /dev/null +++ b/panel/app/translations/id/package.json @@ -0,0 +1,4 @@ +{ + "title": "Bahasa Indonesia", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/it/core.json b/panel/app/translations/it/core.json new file mode 100644 index 0000000..6857e61 --- /dev/null +++ b/panel/app/translations/it/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Annulla", + "add": "Aggiungi", + "addit": "Aggiungi e Modifica ", + "save": "Salva", + "saved": "Salvato!", + "change": "Cambia", + "delete": "Elimina", + "insert": "Inserisci", + "ok": "Ok", + "routes.error.invalid": "URL del Panel non valido", + "controller.error.invalid": "Controller non valido", + "controller.error.action": "Azione non valida", + "view.error.invalid": "Vista non valida", + "options.show": "Mostra opzioni", + "options.hide": "Nascondi opzioni", + "installation": "Installazione", + "installation.check.headline": "Installazione di Kirby Panel", + "installation.check.text": "Kirby ha riscontrato i seguenti problemi durante l'installazione…", + "installation.check.retry": "Riprova", + "installation.check.error": "Sono stati riscontrati dei problemi!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts non dispone dei permessi di scrittura", + "installation.check.error.avatars": "/assets/avatars non dispone dei permessi di scrittura", + "installation.check.error.blueprints": "Aggiungi la cartella /site/blueprints", + "installation.check.error.content": "La cartella /content e tutti i file e le cartelle in essa contenuti devono disporre dei permessi di scrittura.", + "installation.check.error.thumbs": "La cartella /thumbs deve disporre dei permessi di scrittura.", + "installation.signup.username.label": "Crea il tuo primo profilo", + "installation.signup.username.placeholder": "Nome utente", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@esempio.com", + "installation.signup.password.label": "Password", + "installation.signup.language.label": "Lingua", + "installation.signup.button": "Crea un nuovo profilo", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Username", + "login.password.label": "Password", + "login.error": "Username o password non validi", + "login.button": "Log in", + "login.log.error.permissions": "Il file di log degli accessi non è scrivibile.", + "logout": "Log out", + "topbar.error.class.definition": "Manca la definizione topbar per la classe:", + "dashboard": "Dashboard", + "dashboard.index.pages.title": "Pagine", + "dashboard.index.pages.edit": "Modifica", + "dashboard.index.pages.add": "Aggiungi", + "dashboard.index.site.title": "URL del sito", + "dashboard.index.account.title": "Il tuo account", + "dashboard.index.account.edit": "Modifica", + "dashboard.index.metatags.title": "Informazioni sul sito", + "dashboard.index.metatags.edit": "Modifica", + "dashboard.index.history.title": "Le tue ultime modifiche", + "dashboard.index.history.text": "Le ultime pagine modificate verranno mostrate qui per permetterti di ritrovarle più facilmente.", + "dashboard.index.license.title": "Licenza di Kirby", + "dashboard.index.license.text": "Sembra che stai usando Kirby su un server pubblico senza una licenza valida!\n\nPer favore, supporta Kirby (link: {buy} text: comprando subito una licenza)\n\nSe già possiedi una licenza, basta aggiungerla al tuo file di configurazione: (link: {docs} text: site/config/config.php)", + "metatags": "Informazioni sul sito", + "metatags.info": "Kirby info", + "metatags.license": "Licenza di Kirby", + "metatags.version.toolkit": "Versione del Toolkit", + "metatags.version.kirby": "Versione di Kirby", + "metatags.version.panel": "Versione del Panel", + "metatags.back": "Torna alla dashboard", + "metatags.files": "File del sito", + "site.delete.error": "Il sito non può essere rimosso", + "pages.show.settings": "Impostazioni della pagina", + "pages.show.preview": "Anteprima", + "pages.show.template": "Template", + "pages.show.changeurl": "Modifica URL", + "pages.show.invisible": "Status: invisibile", + "pages.show.visible": "Status: visibile", + "pages.show.changes.text": "Ci sono modifiche non salvate!", + "pages.show.changes.button": "Abbandona", + "pages.show.delete": "Elimina questa pagina", + "pages.show.subpages.title": "Pagine", + "pages.show.subpages.edit": "Modifica", + "pages.show.subpages.add": "Aggiungi", + "pages.show.subpages.empty": "Questa pagina non ha sottopagine", + "pages.show.files.title": "File", + "pages.show.files.edit": "Modifica", + "pages.show.files.add": "Aggiungi", + "pages.show.files.empty": "Questa pagina non ha file", + "pages.show.error.permissions.title": "Questa pagina non dispone dei permessi di scrittura", + "pages.show.error.permissions.text": "Verifica i permessi della cartella /content e di tutto il suo contenuto.", + "pages.show.error.permissions.retry": "Riprova", + "pages.show.error.notitle.title": "Nel modello non è previsto un campo per il titolo", + "pages.show.error.notitle.text": "Aggiungi un campo titolo e riprova", + "pages.show.error.notitle.retry": "Riprova", + "pages.show.error.form": "Compila tutti i campi correttamente", + "pages.add.title.label": "Aggiungi una nuova pagina", + "pages.add.title.placeholder": "Titolo", + "pages.add.url.label": "URL", + "pages.add.url.enter": "(inserisci un titolo)", + "pages.add.url.close": "Chiudi", + "pages.add.url.help": "Formato: minuscole a-z, 0-9 e trattini", + "pages.add.template.label": "Template", + "pages.add.error.create": "La pagina non può essere creata", + "pages.add.error.title": "Il titolo è vuoto", + "pages.add.error.template": "Il template non è stato trovato", + "pages.add.error.max.headline": "Non è consentita l'aggiunta di nuove pagine", + "pages.add.error.max.text": "Il numero massimo di sottopagine per questa pagina è stato raggiunto.", + "pages.url.uid.label": "URL", + "pages.url.uid.label.option": "Crea in base al titolo", + "pages.url.error.exists": "Esiste già una pagina con lo stesso URL", + "pages.url.error.move": "L'URL non può essere modificato", + "pages.url.error.rights": "Non puoi cambiare l'URL di questa pagina", + "pages.template.select.label": "Template", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Posizione", + "pages.toggle.invisible": "invisibile", + "pages.toggle.publish": "Vuoi veramente cambiare lo status della pagina a **visibile?**", + "pages.toggle.hide": "Vuoi veramente cambiare lo status della pagina a **invisibile?**", + "pages.toggle.error.error": "Lo status della pagina d'errore non può essere cambiato", + "pages.delete.headline": "Sei sicuro di voler eliminare questa pagina?", + "pages.delete.error.home.headline": "La pagina home non può essere eliminata", + "pages.delete.error.home.text": "Stai cercando di eliminare la pagina home. Quest'operazione non è possibile e potrebbe causare effetti indesiderati.", + "pages.delete.error.error.headline": "La pagina di errore non può essere eliminata", + "pages.delete.error.error.text": "Stai cercando di eliminare la pagina di errore. Quest'operazione non è possibile e potrebbe causare effetti indesiderati.", + "pages.delete.error.children.headline": "La pagina non può essere eliminata", + "pages.delete.error.children.text": "Questa pagina ha delle sottopagine e quindi non può essere eliminata. Elimina prima tutte le sottopagine.", + "pages.delete.error.blocked.headline": "La pagina non può essere eliminata", + "pages.delete.error.blocked.text": "Questa pagina è bloccata e non può essere eliminata.", + "pages.search.help": "Cerca le pagine indicando l'URL. Naviga tra i risultati della ricerca con i tasti freccia e premi invio per visualizzare la pagina selezionata.", + "pages.search.noresults": "La tua ricerca non ha dato risultati. Prova di nuovo con un URL differente.", + "pages.error.missing": "La pagina non è stata trovata", + "subpages": "Pagine", + "subpages.index.headline": "Pagine in", + "subpages.index.back": "Indietro", + "subpages.index.add": "Aggiungi nuova pagina", + "subpages.index.add.first.text": "Questa pagina non ha ancora sottopagine", + "subpages.index.add.first.button": "Aggiungi la prima pagina", + "subpages.index.visible": "Pagine visibili", + "subpages.index.visible.help": "Trascina qui una pagina nascosta per riordinarla/renderla visibile.", + "subpages.index.invisible": "Pagine nascoste", + "subpages.index.invisible.help": "Trascina qui una pagina visibile per nasconderla.", + "subpages.add.error": "A questa pagina non è consentito avere altre sottopagine", + "subpages.add.error.more": "Questa pagina non può avere altre sottopagine", + "subpages.error.missing": "La pagina non è stata trovata", + "files": "Files", + "files.index.headline": "Files associati a", + "files.index.back": "Indietro", + "files.index.upload": "Carica un nuovo file", + "files.index.upload.first.text": "Questa pagina non contiene file", + "files.index.upload.first.button": "Carica il primo file", + "files.index.edit": "Modifica", + "files.index.delete": "Elimina", + "files.index.error.disabled": "Alla pagina non è consentito avere nessun file", + "files.add.error.max": "Il numero massimo di file per l'utente corrente è stato raggiunto.", + "files.add.error.extension.missing": "Non puoi caricare file senza estensione", + "files.add.error.extension.forbidden": "Estensione non consentita", + "files.add.error.mime.forbidden": "Mime type non consentito", + "files.add.error.htaccess": "Il file htaccess non può essere caricato", + "files.add.error.invisible": "I file invisibili non possono essere caricati", + "files.add.blueprint.type.error": "La pagina consente solo:", + "files.add.blueprint.size.error": "La pagina permette una dimensione del file di", + "files.show.name.label": "Nome del file", + "files.show.info.label": "Tipo / Dimensione / Misure", + "files.show.link.label": "Link pubblico", + "files.show.open": "Mostra/scarica file", + "files.show.back": "Indietro", + "files.show.replace": "Sostituisci", + "files.show.delete": "Elimina", + "files.show.error.rename": "Non è stato possibile rinominare il file", + "files.show.error.form": "Compila tutti i campi correttamente", + "files.upload.drop": "Trascina un file qui…", + "files.upload.click": "…o clicca per caricarne uno", + "files.replace.drop": "Trascina un file qui…", + "files.replace.click": "…o clicca per caricarne uno", + "files.replace.error.type": "Il file caricato deve avere lo stesso formato", + "files.delete.headline": "Sei sicuro di voler eliminare questo file?", + "files.error.missing.page": "La pagina non è stata trovata", + "files.error.missing.file": "Il file non è stato trovato", + "users": "Utenti", + "users.index.headline": "Tutti gli utenti", + "users.index.add": "Aggiungi nuovo utente", + "users.index.edit": "Modifica", + "users.index.delete": "Elimina", + "users.form.username.label": "Nome utente", + "users.form.username.placeholder": "Il tuo nome utente", + "users.form.username.help": "Caratteri consentiti: minuscole a-z, 0-9 e trattini", + "users.form.username.readonly": "Il nome utente non può essere modificato", + "users.form.firstname.label": "Nome", + "users.form.lastname.label": "Cognome", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@esempio.com", + "users.form.password.label": "Password", + "users.form.password.confirm.label": "Conferma password", + "users.form.password.new.label": "Nuova password", + "users.form.password.new.confirm.label": "Conferma la nuova password", + "users.form.password.new.help": "Lasciare vuoto per mantenere la password corrente", + "users.form.language.label": "Lingua", + "users.form.role.label": "Ruolo", + "users.form.options.headline": "Opzioni account", + "users.form.options.message": "Invia email", + "users.form.options.delete": "Elimina account", + "users.form.avatar.headline": "Immagine del profilo", + "users.form.avatar.upload": "Carica immagine del profilo", + "users.form.avatar.replace": "Sostituisci immagine del profilo", + "users.form.avatar.delete": "Elimina immagine del profilo", + "users.form.back": "Torna agli utenti", + "users.form.error.password.confirm": "Conferma la password", + "users.form.error.update": "Il profilo dell'utente non può essere aggiornato", + "users.form.error.update.rights": "Non ti è permesso aggiornare questo utente", + "users.form.error.create": "Il profilo dell'utente non può essere creato", + "users.form.error.permissions.title": "La cartella degli account non dispone dei permessi di scrittura.", + "users.form.error.permissions.text": "Verifica che la cartella /site/accounts esista e disponga dei permessi di scrittura.", + "users.delete.headline": "Sei sicuro di voler eliminare questo utente?", + "users.delete.error": "L'utente non può essere eliminato", + "users.delete.error.permission": "Non ti è permesso eliminare gli utenti", + "users.delete.error.permission.single": "Non ti è permesso eliminare questo utente ", + "users.delete.error.lastadmin": "Non puoi eliminare l'ultimo amministratore", + "users.avatar.drop": "Trascina qui un'immagine per il profilo…", + "users.avatar.click": "…o clicca per caricarne una", + "users.avatar.error.type": "Puoi solo caricare files in formato JPG, PNG o GIF.", + "users.avatar.error.folder.headline": "La cartella delle immagini di profilo non ha i permessi di scrittura", + "users.avatar.error.folder.text": "Crea la cartella /assets/avatars e assegnale i permessi di scrittura per caricare le immagini dei profili.", + "users.avatar.error.permission": "Non ti è permesso di cambiare l'avatar", + "users.avatar.delete.error": "L'immagine del profilo non è potuta essere eliminata.", + "users.avatar.delete.error.permission": "Non ti è permesso di eliminare l'avatar di questo utente", + "users.avatar.delete.success": "L'immagine del profilo è stata eliminata", + "users.avatar.missing": "Questo utente non ha un avatar", + "users.error.missing": "L'utente non è stato trovato", + "user.error.lastadmin": "Tu sei l'unico amministratore. Questa voce non può essere cambiata.", + "form.error.missing": "Il form non esiste", + "form.construct.error.invalid": "Il metodo construction del form è invalido ", + "fields.required": "Campo obbligatorio", + "fields.date.label": "Data", + "fields.date.months": [ + "Gennaio", + "Febbraio", + "Marzo", + "Aprile", + "Maggio", + "Giugno", + "Luglio", + "Agosto", + "Settembre", + "Ottobre", + "Novembre", + "Dicembre" + ], + "fields.date.weekdays": [ + "Domenica", + "Lunedì", + "Martedì", + "Mercoledì", + "Giovedì", + "Venerdì", + "Sabato" + ], + "fields.date.weekdays.short": [ + "Do", + "Lu", + "Ma", + "Me", + "Gi", + "Ve", + "Sa" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@esempio.com", + "fields.number.label": "Numero", + "fields.number.placeholder": "#", + "fields.page.label": "Pagina", + "fields.page.placeholder": "percorso/alla/pagina", + "fields.password.label": "Password", + "fields.structure.add": "Aggiungi", + "fields.structure.add.first": "Aggiungi il primo elemento", + "fields.structure.empty": "Non ci sono ancora elementi.", + "fields.structure.entry.error": "L'elemento non esiste", + "fields.structure.cancel": "Annulla", + "fields.structure.save": "Salva", + "fields.structure.edit": "Modifica", + "fields.structure.delete": "Elimina", + "fields.structure.delete.label": "Vuoi veramente eliminare questo elemento?", + "fields.tags.label": "Tag", + "fields.tel.label": "Telefono", + "fields.textarea.buttons.bold.label": "Testo in grassetto", + "fields.textarea.buttons.bold.text": "Testo in grassetto", + "fields.textarea.buttons.italic.label": "Testo in corsivo", + "fields.textarea.buttons.italic.text": "Testo in corsivo", + "fields.textarea.buttons.link.label": "Link", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Immagine", + "fields.textarea.buttons.file.label": "File", + "fields.toggle.yes": "Sì", + "fields.toggle.no": "No", + "fields.toggle.on": "On", + "fields.toggle.off": "Off", + "fields.error.missing.controller": "Manca il file controller del campo", + "fields.error.missing.class": "Manca la classe controller del campo", + "fields.error.route.invalid": "Route del campo invalida", + "fields.error.extended": "Il campo non può essere esteso", + "editor.link.url.label": "Link", + "editor.link.text.label": "Testo del link", + "editor.link.text.help": "Il testo del link è opzionale", + "editor.email.address.label": "Email", + "editor.email.address.placeholder": "mail@esempio.com", + "editor.email.text.label": "Testo del link", + "editor.email.text.help": "Il testo del link è opzionale", + "editor.file.empty": "In questa pagina non ci sono files", + "editor.image.empty": "In questa pagina non ci sono immagini", + "autocomplete.method.error": "Metodo di autocompletamento invalido", + "blueprints.error.default.missing": "Manca il blueprint di default", + "error": "Errore", + "error.headline": "Errore" +} \ No newline at end of file diff --git a/panel/app/translations/it/package.json b/panel/app/translations/it/package.json new file mode 100644 index 0000000..89467f8 --- /dev/null +++ b/panel/app/translations/it/package.json @@ -0,0 +1,4 @@ +{ + "title": "Italiano", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/ja/core.json b/panel/app/translations/ja/core.json new file mode 100644 index 0000000..23aaa52 --- /dev/null +++ b/panel/app/translations/ja/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "キャンセル", + "add": "追加", + "addit": "Add & Edit", + "save": "保存", + "saved": "保存しました", + "change": "更新", + "delete": "削除", + "insert": "挿入", + "ok": "Ok", + "routes.error.invalid": "パネルのURLが間違っています。", + "controller.error.invalid": "不正な controlle です", + "controller.error.action": "不正な actio です", + "view.error.invalid": "不正な view です", + "options.show": "オプションを表示", + "options.hide": "オプションを隠す", + "installation": "インストール", + "installation.check.headline": "Kirby管理パネルのインストール", + "installation.check.text": "Kirbyのインストールプロセスで以下の問題が発生しました", + "installation.check.retry": "リトライ", + "installation.check.error": "次の項目を確認してください。", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts に書き込み権限がありません", + "installation.check.error.avatars": "/assets/avatars に書き込み権限がありません", + "installation.check.error.blueprints": "/site/blueprints フォルダがありません", + "installation.check.error.content": "content フォルダとその中のフォルダ/ファイルは全て書き込み権限を与えてください", + "installation.check.error.thumbs": "thumbs フォルダに書き込み権限がありません", + "installation.signup.username.label": "最初のアカウントを作成します", + "installation.signup.username.placeholder": "ユーザーID", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@example.com", + "installation.signup.password.label": "パスワード", + "installation.signup.language.label": "表示言語", + "installation.signup.button": "アカウントを作成", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "ユーザーID", + "login.password.label": "パスワード", + "login.error": "ユーザーID または パスワード が正しくありません", + "login.button": "Log in", + "login.log.error.permissions": "ログインログファイルに書き込み権限がありません", + "logout": "Log out", + "topbar.error.class.definition": "トップバーのクラス定義ファイルがありません", + "dashboard": "ダッシュボード", + "dashboard.index.pages.title": "ページ", + "dashboard.index.pages.edit": "編集", + "dashboard.index.pages.add": "作成", + "dashboard.index.site.title": "WebサイトURL", + "dashboard.index.account.title": "アカウント", + "dashboard.index.account.edit": "編集", + "dashboard.index.metatags.title": "サイト変数", + "dashboard.index.metatags.edit": "編集", + "dashboard.index.history.title": "最近編集したページ", + "dashboard.index.history.text": "最近編集したページの履歴がここに表示されます。", + "dashboard.index.license.title": "Kirbyのライセンス", + "dashboard.index.license.text": "現在 Kirby はライセンス登録なしにパブリックサーバーにインストールされています。\n\nPlease, support Kirby and (link: {buy} text: ライセンスの購入)により Kirby のサポートをお願いします。\n\nライセンスキーをお持ちでしたら設定ファイル((link: {docs} text: site/config/config.php))に記入してください。", + "metatags": "サイト変数", + "metatags.info": "Kirbyの情報", + "metatags.license": "Kirbyのライセンス", + "metatags.version.toolkit": "Toolkitのバージョン", + "metatags.version.kirby": "Kirbyのバージョン", + "metatags.version.panel": "管理パネルのバージョン", + "metatags.back": "ダッシュボードに戻る", + "metatags.files": "サイトファイル", + "site.delete.error": "サイトは削除できません", + "pages.show.settings": "ページアクション", + "pages.show.preview": "プレビュー", + "pages.show.template": "テンプレート", + "pages.show.changeurl": "ページ識別子(URL)", + "pages.show.invisible": "状態: 管理対象外", + "pages.show.visible": "状態: 管理対象", + "pages.show.changes.text": "保存されていない編集箇所があります!", + "pages.show.changes.button": "編集内容を破棄", + "pages.show.delete": "このページを削除", + "pages.show.subpages.title": "サブページ", + "pages.show.subpages.edit": "編集", + "pages.show.subpages.add": "作成", + "pages.show.subpages.empty": "サブページはありません", + "pages.show.files.title": "ファイル", + "pages.show.files.edit": "編集", + "pages.show.files.add": "アップロード", + "pages.show.files.empty": "ファイルはありません", + "pages.show.error.permissions.title": "書き込み権限がありません", + "pages.show.error.permissions.text": "content フォルダとその中のフォルダ/ファイルは全て書き込み権限を与えてください", + "pages.show.error.permissions.retry": "リトライ", + "pages.show.error.notitle.title": "定義ファイルに title フィールドが定義されていません", + "pages.show.error.notitle.text": "title フィールドを定義した上でリトライしてください", + "pages.show.error.notitle.retry": "リトライ", + "pages.show.error.form": "入力が正しくありません", + "pages.add.title.label": "新規ページ", + "pages.add.title.placeholder": "タイトル", + "pages.add.url.label": "ページ識別子(URL)", + "pages.add.url.enter": "(このページのURLに使用されます)", + "pages.add.url.close": "閉じる", + "pages.add.url.help": "使用可能な文字: 英数小文字 a-z, 0-9 と ハイフン ( - )", + "pages.add.template.label": "テンプレート", + "pages.add.error.create": "ページを作成できませんでした", + "pages.add.error.title": "タイトルが空欄です", + "pages.add.error.template": "テンプレートを指定してください", + "pages.add.error.max.headline": "ページを作成できません", + "pages.add.error.max.text": "上限に達しましたので、このページにはこれ以上サブページを作成できませんでした", + "pages.url.uid.label": "ページ識別子(URL)", + "pages.url.uid.label.option": "ページ名から生成", + "pages.url.error.exists": "同じページ識別子(URL)を持つページが既に存在しています", + "pages.url.error.move": "ページ識別子(URL)を変更できませんでした", + "pages.url.error.rights": "You cannot change the URL of this page", + "pages.template.select.label": "テンプレート", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "ページ順序", + "pages.toggle.invisible": "管理対象外", + "pages.toggle.publish": "Do you really want to change the status of this page to **visible?**", + "pages.toggle.hide": "Do you really want to change the status of this page to **invisible?**", + "pages.toggle.error.error": "The status of the error page cannot be changed", + "pages.delete.headline": "このページを削除してもよろしいですか?", + "pages.delete.error.home.headline": "The home page cannot be deleted", + "pages.delete.error.home.text": "You are trying to delete the home page. This is not possible and would lead to unwanted effects.", + "pages.delete.error.error.headline": "The error page cannot be deleted", + "pages.delete.error.error.text": "You are trying to delete the error page. This is not possible and would lead to unwanted effects.", + "pages.delete.error.children.headline": "The page cannot be deleted", + "pages.delete.error.children.text": "This page has subpages and cannot be deleted. Please delete all subpages first.", + "pages.delete.error.blocked.headline": "The page cannot be deleted", + "pages.delete.error.blocked.text": "This page is locked and cannot be deleted.", + "pages.search.help": "Search pages by URL. Navigate through search results with your up and down arrow keys and hit enter to jump to the selected page.", + "pages.search.noresults": "There are no search results for your query. Please try again with a different URL.", + "pages.error.missing": "The page could not be found", + "subpages": "ページ", + "subpages.index.headline": "ページの一覧:", + "subpages.index.back": "戻る", + "subpages.index.add": "新規ページ", + "subpages.index.add.first.text": "このページにはサブページがありません", + "subpages.index.add.first.button": "サブページを作成", + "subpages.index.visible": "管理対象のページ", + "subpages.index.visible.help": "管理対象外のページをここにドラッグアンドドロップで管理対象に設定されます。ソート順は設定ファイルに準じます。", + "subpages.index.invisible": "管理対象外のページ", + "subpages.index.invisible.help": "管理対象のページをここにドラッグアンドドロップで管理対象外に設定されます。ソート順はクリアされます。", + "subpages.add.error": "This page is not allowed to have subpages", + "subpages.add.error.more": "This page cannot have any more subpages", + "subpages.error.missing": "ページが見つかりませんでした", + "files": "ファイル", + "files.index.headline": "ファイルの一覧:", + "files.index.back": "戻る", + "files.index.upload": "ファイルのアップロード", + "files.index.upload.first.text": "このページにはファイルがありません", + "files.index.upload.first.button": "ファイルのアップロード", + "files.index.edit": "編集", + "files.index.delete": "削除", + "files.index.error.disabled": "The page is not allowed to have any files", + "files.add.error.max": "The maximum number of files for the current page has been reached.", + "files.add.error.extension.missing": "You cannot upload files without extension", + "files.add.error.extension.forbidden": "Forbidden file extension", + "files.add.error.mime.forbidden": "Forbidden mime type", + "files.add.error.htaccess": "htaccess files cannot be uploaded", + "files.add.error.invisible": "Invisible files cannot be uploaded", + "files.add.blueprint.type.error": "Page only allows:", + "files.add.blueprint.size.error": "Page only allows file size of", + "files.show.name.label": "ファイル名", + "files.show.info.label": "Type / Size / Dimensions", + "files.show.link.label": "Public link", + "files.show.open": "Show/download file", + "files.show.back": "Back", + "files.show.replace": "Replace", + "files.show.delete": "Delete", + "files.show.error.rename": "ファイル名を変更できませんでした", + "files.show.error.form": "入力が正しくありません", + "files.upload.drop": "ファイルをここにドロップ", + "files.upload.click": "もしくは、クリックしてアップロード", + "files.replace.drop": "ファイルをここにドロップ", + "files.replace.click": "もしくは、クリックして置き換え", + "files.replace.error.type": "元と同じ種類のファイルを選択してください", + "files.delete.headline": "このファイルを削除してもよろしいですか?", + "files.error.missing.page": "ページが見つかりませんでした", + "files.error.missing.file": "ファイルが見つかりませんでした", + "users": "ユーザー", + "users.index.headline": "ユーザーの一覧", + "users.index.add": "新規ユーザー", + "users.index.edit": "編集", + "users.index.delete": "削除", + "users.form.username.label": "ユーザーID", + "users.form.username.placeholder": "Your username", + "users.form.username.help": "使用可能な文字: 英数小文字 a-z, 0-9 と ハイフン ( - )", + "users.form.username.readonly": "ユーザーIDは変更できません", + "users.form.firstname.label": "姓", + "users.form.lastname.label": "名", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@example.com", + "users.form.password.label": "パスワード", + "users.form.password.confirm.label": "パスワードを再入力", + "users.form.password.new.label": "新しいパスワード", + "users.form.password.new.confirm.label": "新しいパスワードを再入力", + "users.form.password.new.help": "変更しない場合は入力しない", + "users.form.language.label": "表示言語", + "users.form.role.label": "権限", + "users.form.options.headline": "アカウントオプション", + "users.form.options.message": "Emailを送る", + "users.form.options.delete": "アカウントを削除", + "users.form.avatar.headline": "プロフィール用画像", + "users.form.avatar.upload": "画像のアップロード", + "users.form.avatar.replace": "画像の変更", + "users.form.avatar.delete": "画像の削除", + "users.form.back": "ユーザーの一覧に戻る", + "users.form.error.password.confirm": "パスワードを確認してください", + "users.form.error.update": "ユーザー情報は更新できませんでした", + "users.form.error.update.rights": "このユーザーの情報を編集する権限がありません", + "users.form.error.create": "新規ユーザーは作成できませんでした", + "users.form.error.permissions.title": "アカウント用のフォルダに書き込み権限がありません", + "users.form.error.permissions.text": "/site/accounts が存在し、書き込み権限があることを確認してください", + "users.delete.headline": "このユーザーを削除してもよろしいですか?", + "users.delete.error": "ユーザーを削除できませんでした", + "users.delete.error.permission": "ユーザーを削除する権限がありません", + "users.delete.error.permission.single": "このユーザーを削除する権限がありません", + "users.delete.error.lastadmin": "最後のAdminユーザーは削除できません", + "users.avatar.drop": "画像をここにドロップ", + "users.avatar.click": "もしくは、クリックしてアップロード", + "users.avatar.error.type": "JPG, PNG および GIF 形式のファイルのみアップロード可能です", + "users.avatar.error.folder.headline": "avatar フォルダに書き込み権限がありません", + "users.avatar.error.folder.text": "プロフィール用画像格納用に /assets/avatars フォルダを作成し、書き込み権限を与えてください", + "users.avatar.error.permission": "プロフィール用画像を変更する権限がありません", + "users.avatar.delete.error": "プロフィール用画像を削除できませんでした", + "users.avatar.delete.error.permission": "このユーザーのプロフィール用画像を削除する権限がありません", + "users.avatar.delete.success": "プロフィール用画像を削除しました", + "users.avatar.missing": "このユーザーはプロフィール用画像が設定されていません", + "users.error.missing": "ユーザーが見つかりませんでした", + "user.error.lastadmin": "現在あなたが唯一のAdminユーザーです。変更することはできません", + "form.error.missing": "The form cannot be found", + "form.construct.error.invalid": "Invalid form construction method", + "fields.required": "必須", + "fields.date.label": "Date", + "fields.date.months": [ + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月" + ], + "fields.date.weekdays": [ + "日曜日", + "月曜日", + "火曜日", + "水曜日", + "木曜日", + "金曜日", + "土曜日" + ], + "fields.date.weekdays.short": [ + "日", + "月", + "火", + "水", + "木", + "金", + "土" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@example.com", + "fields.number.label": "数値", + "fields.number.placeholder": "整数値", + "fields.page.label": "ページ", + "fields.page.placeholder": "path/to/page", + "fields.password.label": "パスワード", + "fields.structure.add": "作成", + "fields.structure.add.first": "最初のカードを作成してください。", + "fields.structure.empty": "カードがありません。", + "fields.structure.entry.error": "The item could not be found", + "fields.structure.cancel": "キャンセル", + "fields.structure.save": "保存", + "fields.structure.edit": "編集", + "fields.structure.delete": "削除", + "fields.structure.delete.label": "このカードを削除してもよろしいですか?", + "fields.tags.label": "タグ", + "fields.tel.label": "電話番号", + "fields.textarea.buttons.bold.label": "強調", + "fields.textarea.buttons.bold.text": "強調", + "fields.textarea.buttons.italic.label": "斜体", + "fields.textarea.buttons.italic.text": "斜体", + "fields.textarea.buttons.link.label": "リンク", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "画像", + "fields.textarea.buttons.file.label": "ファイル", + "fields.toggle.yes": "はい", + "fields.toggle.no": "いいえ", + "fields.toggle.on": "オン", + "fields.toggle.off": "オフ", + "fields.error.missing.controller": "The field controller file is missing", + "fields.error.missing.class": "The field controller class is missing", + "fields.error.route.invalid": "Invalid field route", + "fields.error.extended": "The field cannot be extended", + "editor.link.url.label": "URLを入力", + "editor.link.text.label": "リンクテキスト", + "editor.link.text.help": "リンクテキストは任意で設定可能です", + "editor.email.address.label": "Emailアドレスを入力してください", + "editor.email.address.placeholder": "mail@example.com", + "editor.email.text.label": "リンクテキスト", + "editor.email.text.help": "リンクテキストは任意で設定可能です", + "editor.file.empty": "'このページにはファイルがありません',", + "editor.image.empty": "'このページには画像がありません',", + "autocomplete.method.error": "オートコンプリートがエラーを起こしました", + "blueprints.error.default.missing": "デフォルトの定義ファイル(/site/blueprints/default.php)がありません", + "error": "エラー", + "error.headline": "エラー" +} \ No newline at end of file diff --git a/panel/app/translations/ja/package.json b/panel/app/translations/ja/package.json new file mode 100644 index 0000000..1c20e5e --- /dev/null +++ b/panel/app/translations/ja/package.json @@ -0,0 +1,4 @@ +{ + "title": "Japanese", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/nb/core.json b/panel/app/translations/nb/core.json new file mode 100644 index 0000000..c323d1e --- /dev/null +++ b/panel/app/translations/nb/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Avbryt", + "add": "Legg til", + "addit": "Legg til og rediger", + "save": "Lagre", + "saved": "Lagret!", + "change": "Endre", + "delete": "Slett", + "insert": "Sett Inn", + "ok": "Ok", + "routes.error.invalid": "Ugylid Panel URL", + "controller.error.invalid": "Ugyldig kontroller", + "controller.error.action": "Ugyldig handling", + "view.error.invalid": "Ugyldig view:", + "options.show": "Vis alternativer", + "options.hide": "Skjul alternativer", + "installation": "Installasjon", + "installation.check.headline": "Kirby Panel Installasjon", + "installation.check.text": "Det oppsto problemer under installasjonen av Kirby…", + "installation.check.retry": "Prøv på nytt", + "installation.check.error": "Det er noen problemer!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts er ikke skrivbar", + "installation.check.error.avatars": "/assets/avatars er ikke skrivbar", + "installation.check.error.blueprints": "Vennligst legg til en /site/blueprints mappe", + "installation.check.error.content": "Mappen content og alt av innhold må være skrivbar.", + "installation.check.error.thumbs": "Mappen thumbs må være skrivbar.", + "installation.signup.username.label": "Lag din første konto", + "installation.signup.username.placeholder": "Brukernavn", + "installation.signup.email.label": "Epost", + "installation.signup.email.placeholder": "epost@eksempel.no", + "installation.signup.password.label": "Passord", + "installation.signup.language.label": "Språk", + "installation.signup.button": "Opprett konto", + "login": "Logg Inn", + "login.welcome": "Vennligst logg inn med din nye konto", + "login.username.label": "Brukernavn", + "login.password.label": "Passord", + "login.error": "Brukernavn eller passord er feil", + "login.button": "Logg Inn", + "login.log.error.permissions": "Logg inn loggfilen er ikke skrivbar.", + "logout": "Logg ut", + "topbar.error.class.definition": "Mangler topbar definisjon for klassen:", + "dashboard": "Dashboard", + "dashboard.index.pages.title": "Sider", + "dashboard.index.pages.edit": "Rediger", + "dashboard.index.pages.add": "Legg til", + "dashboard.index.site.title": "Din side's URL", + "dashboard.index.account.title": "Din konto", + "dashboard.index.account.edit": "Rediger", + "dashboard.index.metatags.title": "Nettsted variabler", + "dashboard.index.metatags.edit": "Rediger", + "dashboard.index.history.title": "Dine siste endringer", + "dashboard.index.history.text": "Dine siste endrede sider vil vises her for å gjøre det enkelt å finne dem igjen senere.", + "dashboard.index.license.title": "Kirby lisens", + "dashboard.index.license.text": "Det ser ut som om du kjører Kirby på en offentlig server uten en gyldig lisens!\n\nVær så snill, støtt Kirby og (link: {buy} text: kjøp en lisens nå)\n\nOm du allerede har en lisens nøkkel, legg den til i din konfigurasjons fil: (link {docs} text: site/config/config.php)", + "metatags": "Nettsted variabler", + "metatags.info": "Kirby informasjon", + "metatags.license": "Kirby lisens", + "metatags.version.toolkit": "Toolkit versjon", + "metatags.version.kirby": "Kirby versjon", + "metatags.version.panel": "Panel versjon", + "metatags.back": "Tilbake til dashboardet", + "metatags.files": "Sidefiler", + "site.delete.error": "Siden kan ikke slettes", + "pages.show.settings": "Sideinnstillinger", + "pages.show.preview": "Åpne forhåndsvisning", + "pages.show.template": "Mal", + "pages.show.changeurl": "Endre URL", + "pages.show.invisible": "Status: usynlig", + "pages.show.visible": "Status: synlig", + "pages.show.changes.text": "Du har ulagrede endringer!", + "pages.show.changes.button": "Forkast", + "pages.show.delete": "Slett denne siden", + "pages.show.subpages.title": "Sider", + "pages.show.subpages.edit": "Rediger", + "pages.show.subpages.add": "Legg til", + "pages.show.subpages.empty": "Denne siden har ingen undersider", + "pages.show.files.title": "Filer", + "pages.show.files.edit": "Rediger", + "pages.show.files.add": "Legg til", + "pages.show.files.empty": "Denne siden har ingen filer", + "pages.show.error.permissions.title": "Siden er ikke skrivbar", + "pages.show.error.permissions.text": "Vennligst sjekk rettigheten for content mappen og filer.", + "pages.show.error.permissions.retry": "Prøv på nytt", + "pages.show.error.notitle.title": "Blueprint har ikke tittelfelt", + "pages.show.error.notitle.text": "Vennligst legg til ett tittelfelt og prøv igjen", + "pages.show.error.notitle.retry": "Prøv igjen", + "pages.show.error.form": "Vennligst fyll inn alle feltene korrekt", + "pages.add.title.label": "Legg til en ny side", + "pages.add.title.placeholder": "Tittel", + "pages.add.url.label": "URL-appendiks", + "pages.add.url.enter": "(skriv inn din tittel)", + "pages.add.url.close": "Lukk", + "pages.add.url.help": "Format: små bokstaver a-z, 0-9 og vanlige bindestreker", + "pages.add.template.label": "Mal", + "pages.add.error.create": "Siden kunne ikke opprettes", + "pages.add.error.title": "Tittelen mangler", + "pages.add.error.template": "Malen mangler", + "pages.add.error.max.headline": "Ingen nye sider tillat", + "pages.add.error.max.text": "Maksimalt antall undersider for den gjeldende siden er nådd.", + "pages.url.uid.label": "URL-appendiks", + "pages.url.uid.label.option": "Opprett fra tittel", + "pages.url.error.exists": "En side med samme appendiks finnes allerede", + "pages.url.error.move": "Appendiks kunne ikke bli endret", + "pages.url.error.rights": "Du kan ikke endre URLen for denne siden", + "pages.template.select.label": "Mal", + "pages.template.warning.text": "Disse feltene vil endres når du endrer mal", + "pages.template.warning.removed": "Fjernet felt", + "pages.template.warning.replaced": "Erstattet felt", + "pages.template.warning.added": "Nye felt", + "pages.template.error": "Malen for denne siden kan ikke endres", + "pages.toggle.position": "Posisjon", + "pages.toggle.invisible": "usynlig", + "pages.toggle.publish": "Vil du virkelig endre statusen for denne siden til **synlig?**", + "pages.toggle.hide": "Vil du virkelig endre statusen for denne siden til **usynlig?**", + "pages.toggle.error.error": "Statusen for error siden kan ikke bli endret", + "pages.delete.headline": "Vil du virkelig slette denne siden?", + "pages.delete.error.home.headline": "Startsiden kan ikke slettes", + "pages.delete.error.home.text": "Du prøver å slette startsiden. Dette er ikke mulig og vil lede til uønskede effekter.", + "pages.delete.error.error.headline": "Feil siden kan ikke slette", + "pages.delete.error.error.text": "Du prøver å slette feil siden. Dette er ikke mulig og vil lede til uønskede effekter..", + "pages.delete.error.children.headline": "Denne siden kan ikke slette", + "pages.delete.error.children.text": "Denne siden har undersider og kan ikke bli slette. Vennligst slett alle undersider først.", + "pages.delete.error.blocked.headline": "Denne siden kan ikke slette", + "pages.delete.error.blocked.text": "Denne siden er låst og kan ikke slettes.", + "pages.search.help": "Søk sider med URL. Naviger gjennom søkeresultatene med opp og ned piltastene og trykk enter for å gå til den valgte siden", + "pages.search.noresults": "Det finnes ingen resultater for søket ditt. Vennligst prøv igjen med en annen nettadresse", + "pages.error.missing": "Siden kunne ikke bli funnet", + "subpages": "Sider", + "subpages.index.headline": "Sider i", + "subpages.index.back": "Tilbake", + "subpages.index.add": "Legg til en ny side", + "subpages.index.add.first.text": "Denne siden har ingen undersider ennå", + "subpages.index.add.first.button": "Legg til den første siden", + "subpages.index.visible": "Synlige sider", + "subpages.index.visible.help": "Dra usynlige sider her for å sortere dem/gjøre dem synlige.", + "subpages.index.invisible": "Usynlige sider", + "subpages.index.invisible.help": "Dra synlige sider her for å sortere dem/gjøre dem usynlige.", + "subpages.add.error": "Denne siden er ikke tillatt å ha undersider", + "subpages.add.error.more": "Denne siden kan ikke å ha flere undersider", + "subpages.error.missing": "Siden kunne ikke bli funnet", + "files": "Filer", + "files.index.headline": "Filer for", + "files.index.back": "Tilbake", + "files.index.upload": "Last opp en ny fil", + "files.index.upload.first.text": "Denne siden har ingen filer ennå", + "files.index.upload.first.button": "Last opp den første filen", + "files.index.edit": "Rediger", + "files.index.delete": "Slett", + "files.index.error.disabled": "Denne siden er ikke tillatt å ha filer", + "files.add.error.max": "Maksimalt antall filer for den gjeldende siden er nådd.", + "files.add.error.extension.missing": "Du kan ikke laste opp filer uten filtype", + "files.add.error.extension.forbidden": "Ugyldig filtype", + "files.add.error.mime.forbidden": "Ugyldig MIME-type", + "files.add.error.htaccess": "htaccess filer kan ikke bli lastet opp", + "files.add.error.invisible": "Usynlige filer kan ikke bli lastet opp", + "files.add.blueprint.type.error": "Siden godtar kun:", + "files.add.blueprint.size.error": "Siden tillater bare filstørrelsen:", + "files.show.name.label": "Filnavn", + "files.show.info.label": "Type / Størrelse / Dimensjoner", + "files.show.link.label": "Offentlig link", + "files.show.open": "Vis/last ned fil", + "files.show.back": "Tilbake", + "files.show.replace": "Erstatt", + "files.show.delete": "Slett", + "files.show.error.rename": "Filen kunne ikke endre navn", + "files.show.error.form": "Vennligst fyll inn alle feltene korrekt", + "files.upload.drop": "Slipp filene her…", + "files.upload.click": "…eller klikk for å laste opp", + "files.replace.drop": "Slipp en fil her…", + "files.replace.click": "…eller klikk for å erstatte", + "files.replace.error.type": "Den opplastede filen må ha samme filtype", + "files.delete.headline": "Vil du virkelig slette denne filen?", + "files.error.missing.page": "Siden kunne ikke bli funnet", + "files.error.missing.file": "Filen kunne ikke bli funnet", + "users": "Brukere", + "users.index.headline": "Alle brukere", + "users.index.add": "Legg til en ny bruker", + "users.index.edit": "Rediger", + "users.index.delete": "Slett", + "users.form.username.label": "Brukernavn", + "users.form.username.placeholder": "Ditt brukernavn", + "users.form.username.help": "Tillatte tegn: små bokstaver a-z, 0-9 og vanlige bindestreker", + "users.form.username.readonly": "Brukernavnet kan ikke endres", + "users.form.firstname.label": "Fornavn", + "users.form.lastname.label": "Etternavn", + "users.form.email.label": "Epost", + "users.form.email.placeholder": "epost@eksempel.no", + "users.form.password.label": "Passord", + "users.form.password.confirm.label": "Bekreft passord", + "users.form.password.new.label": "Nytt passord", + "users.form.password.new.confirm.label": "Bekreft det nye passordet", + "users.form.password.new.help": "La stå tomt for å beholde det gjeldende passord", + "users.form.language.label": "Språk", + "users.form.role.label": "Rolle", + "users.form.options.headline": "Kontoalternativer", + "users.form.options.message": "Send epost", + "users.form.options.delete": "Slett konto", + "users.form.avatar.headline": "Profil bilde", + "users.form.avatar.upload": "Last opp ett profil bilde", + "users.form.avatar.replace": "Erstatt profil bildet", + "users.form.avatar.delete": "Slett profil bildet", + "users.form.back": "Tilbake til brukere", + "users.form.error.password.confirm": "Vennligst bekreft passordet", + "users.form.error.update": "Brukeren kunne ikke bli oppdatert", + "users.form.error.update.rights": "Du er ikke tillat til å oppdatere denne brukeren", + "users.form.error.create": "Brukeren kunne ikke bli opprettes", + "users.form.error.permissions.title": "Account mappen er ikke skrivbar", + "users.form.error.permissions.text": "Vennligst kontroller at /site/accounts eksiterer og er skrivbar.", + "users.delete.headline": "Vil du virkelig slette denne konten?", + "users.delete.error": "Denne brukeren kunne ikke bli slettet", + "users.delete.error.permission": "Du er ikke tillat til å slette brukere", + "users.delete.error.permission.single": "Du er ikke tillat å slette denne brukeren", + "users.delete.error.lastadmin": "Du kan ikke slette den siste admin", + "users.avatar.drop": "Slipp profil bildet her…", + "users.avatar.click": "…eller klikk for å laste opp", + "users.avatar.error.type": "Du kan kun laste opp JPG, PNG og GIF filer", + "users.avatar.error.folder.headline": "Avatar mappen er ikke skrivbar", + "users.avatar.error.folder.text": "Vennligst opprett mappen /assets/avatars og kontroller att den er skrivbar for å laste opp profil bilder.", + "users.avatar.error.permission": "Du er ikke tillat til å endre avatar", + "users.avatar.delete.error": "Profil bildet kunne ikke bli slette", + "users.avatar.delete.error.permission": "Du er ikke tillat til å slette avataren til denne brukeren", + "users.avatar.delete.success": "Profil bildet har blitt slettet", + "users.avatar.missing": "Denne brukerer har ingen avatar", + "users.error.missing": "Brukeren kunne ikke bli funnet", + "user.error.lastadmin": "Du er den eneste administrator. Dette kan ikke endres.", + "form.error.missing": "Skjemaet kan ikke funnet", + "form.construct.error.invalid": "Ugyldig skjema byggemåte", + "fields.required": "Påkrevd", + "fields.date.label": "Dato", + "fields.date.months": [ + "Januar", + "Februar", + "Mars", + "April", + "Mai", + "Juni", + "July", + "August", + "September", + "Oktober", + "November", + "Desember" + ], + "fields.date.weekdays": [ + "Søndag", + "Mandag", + "Tirsdag", + "Onsdag", + "Torsdag", + "Fredag", + "Lørdag" + ], + "fields.date.weekdays.short": [ + "Søn", + "Man", + "Tir", + "Ons", + "Tor", + "Fre", + "Lør" + ], + "fields.email.label": "Epost", + "fields.email.placeholder": "epost@eksempel.no", + "fields.number.label": "Nummer", + "fields.number.placeholder": "#", + "fields.page.label": "Side", + "fields.page.placeholder": "sti/til/side", + "fields.password.label": "Passord", + "fields.structure.add": "Legg til", + "fields.structure.add.first": "Legg til den første oppføringen", + "fields.structure.empty": "Ingen oppføringer enda", + "fields.structure.entry.error": "Elementet kunne ikke bli funnet", + "fields.structure.cancel": "Avbryt", + "fields.structure.save": "Lagre", + "fields.structure.edit": "Rediger", + "fields.structure.delete": "Slett", + "fields.structure.delete.label": "Ønsker du virkelig å slette denne oppføringen?", + "fields.tags.label": "Tagger", + "fields.tel.label": "Mobil", + "fields.textarea.buttons.bold.label": "Tykk tekst", + "fields.textarea.buttons.bold.text": "Tykk tekst", + "fields.textarea.buttons.italic.label": "Kursiv tekst", + "fields.textarea.buttons.italic.text": "Kursiv tekst", + "fields.textarea.buttons.link.label": "Adresse", + "fields.textarea.buttons.email.label": "Epost", + "fields.textarea.buttons.image.label": "Bilde", + "fields.textarea.buttons.file.label": "Fil", + "fields.toggle.yes": "Ja", + "fields.toggle.no": "Nei", + "fields.toggle.on": "På", + "fields.toggle.off": "Av", + "fields.error.missing.controller": "Felt kontroller filen mangler", + "fields.error.missing.class": "Felt kontroller klassen mangler", + "fields.error.route.invalid": "Ugyldig felt rute", + "fields.error.extended": "Feltet kan ikke bli utvidet", + "editor.link.url.label": "Sett inn URL", + "editor.link.text.label": "Koblingstekst", + "editor.link.text.help": "Koblingstekst er valgfri", + "editor.email.address.label": "Sett inn epost adresse", + "editor.email.address.placeholder": "epost@eksempel.no", + "editor.email.text.label": "Koblingstekst", + "editor.email.text.help": "Koblingstekst er valgfri", + "editor.file.empty": "Denne siden har ingen filer", + "editor.image.empty": "Denne siden har ingen bilder", + "autocomplete.method.error": "Ugyldig autocomplete metode", + "blueprints.error.default.missing": "Mangler standard blåkopi", + "error": "Feil", + "error.headline": "Feil" +} \ No newline at end of file diff --git a/panel/app/translations/nb/package.json b/panel/app/translations/nb/package.json new file mode 100644 index 0000000..72ddcc6 --- /dev/null +++ b/panel/app/translations/nb/package.json @@ -0,0 +1,4 @@ +{ + "title": "Norsk Bokmål", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/nl/core.json b/panel/app/translations/nl/core.json new file mode 100644 index 0000000..b713063 --- /dev/null +++ b/panel/app/translations/nl/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Annuleren", + "add": "Toevoegen", + "addit": "Toevoegen en bewerken", + "save": "Opslaan", + "saved": "Opgeslagen!", + "change": "Wijzig", + "delete": "Verwijder", + "insert": "Toevoegen", + "ok": "Ok", + "routes.error.invalid": "Ongeldige Panel URL", + "controller.error.invalid": "Ongeldige controller", + "controller.error.action": "Ongeldige actie", + "view.error.invalid": "Ongeldige view:", + "options.show": "Toon opties", + "options.hide": "Verberg opties", + "installation": "Installatie", + "installation.check.headline": "Kirby Panel installatie", + "installation.check.text": "Kirby vond de volgende fouten tijdens de installatie...", + "installation.check.retry": "Opnieuw proberen", + "installation.check.error": "Er zijn een aantal problemen!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts heeft geen schrijfrechten", + "installation.check.error.avatars": "/assets/avatars heeft geen schrijfrechten", + "installation.check.error.blueprints": "Voeg een /site/blueprints map toe", + "installation.check.error.content": "De contentmap en alle bestanden hierin moeten schrijfrechten hebben.", + "installation.check.error.thumbs": "De thumbs-map moet schrijfrechten hebben.", + "installation.signup.username.label": "Maak je eerste account", + "installation.signup.username.placeholder": "Gebruikersnaam", + "installation.signup.email.label": "E-mailadres", + "installation.signup.email.placeholder": "mail@voorbeeld.nl", + "installation.signup.password.label": "Wachtwoord", + "installation.signup.language.label": "Taal", + "installation.signup.button": "Maak account aan", + "login": "Inloggen", + "login.welcome": "Log in met je nieuwe account", + "login.username.label": "Gebruikersnaam", + "login.password.label": "Wachtwoord", + "login.error": "Ongeldige gebruikersnaam of wachtwoord", + "login.button": "Inloggen", + "login.log.error.permissions": "Login logbestand heeft geen schrijfrecht.", + "logout": "Uitloggen", + "topbar.error.class.definition": "Ontbrekende 'topbar definition' voor class:", + "dashboard": "Dashboard", + "dashboard.index.pages.title": "Pagina's", + "dashboard.index.pages.edit": "Wijzig", + "dashboard.index.pages.add": "Toevoegen", + "dashboard.index.site.title": "URL van je site", + "dashboard.index.account.title": "Jouw account", + "dashboard.index.account.edit": "Wijzig", + "dashboard.index.metatags.title": "Site-variabelen", + "dashboard.index.metatags.edit": "Wijzig", + "dashboard.index.history.title": "Jouw laatste updates", + "dashboard.index.history.text": "Pagina's die het laatst door jou zijn gewijzigd komen hier te staan, om ze weer makkelijk terug te kunnen vinden.", + "dashboard.index.license.title": "Kirby licentie", + "dashboard.index.license.text": "Het lijkt erop dat je Kirby gebruikt op een publieke server zonder een geldige licentie!\nOndersteun Kirby en (link: {buy} text: koop een licentie)\nAls je al een licentie code hebt, voeg deze dan toe aan je configuratie bestand: (link: {docs} text: site/config/config.php)", + "metatags": "Site-variabelen", + "metatags.info": "Kirby info", + "metatags.license": "Kirby licentie", + "metatags.version.toolkit": "Toolkit versie", + "metatags.version.kirby": "Kirby versie", + "metatags.version.panel": "Panel versie", + "metatags.back": "Terug naar het dashboard", + "metatags.files": "Site bestanden", + "site.delete.error": "De site kan niet worden verwijderd", + "pages.show.settings": "Pagina opties", + "pages.show.preview": "Open voorbeeld", + "pages.show.template": "Template", + "pages.show.changeurl": "Verander URL", + "pages.show.invisible": "Status: onzichtbaar", + "pages.show.visible": "Status: zichtbaar", + "pages.show.changes.text": "Je hebt wijzigingen die nog niet zijn opgeslagen!", + "pages.show.changes.button": "Annuleren", + "pages.show.delete": "Verwijder deze pagina", + "pages.show.subpages.title": "Pagina's", + "pages.show.subpages.edit": "Wijzig", + "pages.show.subpages.add": "Toevoegen", + "pages.show.subpages.empty": "Deze pagina heeft geen subpagina's", + "pages.show.files.title": "Bestanden", + "pages.show.files.edit": "Wijzig", + "pages.show.files.add": "Toevoegen", + "pages.show.files.empty": "Deze pagina heeft geen bestanden", + "pages.show.error.permissions.title": "Deze pagina is niet te bewerken", + "pages.show.error.permissions.text": "Controleer de rechten voor de content-map en alle bestanden in de map.", + "pages.show.error.permissions.retry": "Opnieuw proberen", + "pages.show.error.notitle.title": "De blueprint heeft geen \\`title\\`-veld", + "pages.show.error.notitle.text": "Voeg een \\`title\\` veld toe en probeer het opnieuw.", + "pages.show.error.notitle.retry": "Opnieuw proberen", + "pages.show.error.form": "Vul alle velden correct in", + "pages.add.title.label": "Voeg een nieuwe pagina toe", + "pages.add.title.placeholder": "Titel", + "pages.add.url.label": "URL-toevoeging", + "pages.add.url.enter": "(Voer een titel in)", + "pages.add.url.close": "Sluiten", + "pages.add.url.help": "Toegestaan: a-z (kleine letters) en standaard streepjes (-)", + "pages.add.template.label": "Template", + "pages.add.error.create": "De pagina kon niet worden aangemaakt", + "pages.add.error.title": "De titel ontbreekt", + "pages.add.error.template": "De template ontbreekt", + "pages.add.error.max.headline": "Geen nieuwe pagina's toegestaan", + "pages.add.error.max.text": "Je hebt het maximum aantal van subpagina's voor deze pagina bereikt.", + "pages.url.uid.label": "URL-toevoeging", + "pages.url.uid.label.option": "Maak op basis van titel", + "pages.url.error.exists": "Er bestaat al een pagina met deze URL-toevoeging", + "pages.url.error.move": "De URL-toevoeging kan niet worden gewijzigd", + "pages.url.error.rights": "Je kunt de URL van deze pagina niet wijzigen", + "pages.template.select.label": "Template", + "pages.template.warning.text": "De volgende velden veranderen, als je de template wijzigt.", + "pages.template.warning.removed": "Verwijderde velden", + "pages.template.warning.replaced": "Vervangen velden", + "pages.template.warning.added": "Toegevoegde velden", + "pages.template.error": "De template voor deze pagina kan niet worden gewijzigd", + "pages.toggle.position": "Positie", + "pages.toggle.invisible": "Onzichtbaar", + "pages.toggle.publish": "Wil je de status wijzigen naar **zichtbaar**?", + "pages.toggle.hide": "Wil je de status wijzigen naar **onzichtbaar**?", + "pages.toggle.error.error": "De status van de error-pagina kan niet gewijzigd worden", + "pages.delete.headline": "Weet je zeker dat je deze pagina wil verwijderen?", + "pages.delete.error.home.headline": "De homepage kan niet worden verwijderd", + "pages.delete.error.home.text": "Je probeert de homepage te verwijderen. Dit is niet mogelijk en zou kunnen leiden tot ongewenste resultaten.", + "pages.delete.error.error.headline": "De fout-pagina kan niet worden verwijderd", + "pages.delete.error.error.text": "Je probeert de fout-pagina te verwijderen. Dit is niet mogelijk en zou kunnen leiden tot ongewenste resultaten.", + "pages.delete.error.children.headline": "De pagina kan niet worden verwijderd", + "pages.delete.error.children.text": "Deze pagina heeft subpagina's en kan niet worden verwijderd. Verwijder eerst de subpagina's.", + "pages.delete.error.blocked.headline": "De pagina kan niet worden verwijderd", + "pages.delete.error.blocked.text": "Deze pagina is vergrendeld en kan niet worden verwijderd.", + "pages.search.help": "Zoek door pagina's op URL. Je kunt navigeren tussen de pagina's met je pijltjestoetsen. Met enter ga je naar de geselecteerde pagina.", + "pages.search.noresults": "Geen zoekresultaten. Probeer het opnieuw met een andere URL.", + "pages.error.missing": "De pagina kan niet worden gevonden", + "subpages": "Pagina's", + "subpages.index.headline": "Pagina's in", + "subpages.index.back": "Terug", + "subpages.index.add": "Nieuwe pagina toevoegen", + "subpages.index.add.first.text": "Deze pagina heeft nog geen subpagina's", + "subpages.index.add.first.button": "Voeg de eerste pagina toe", + "subpages.index.visible": "Zichtbare pagina's", + "subpages.index.visible.help": "Sleep onzichtbare pagina's hiernaartoe om ze zichtbaar te maken.", + "subpages.index.invisible": "Onzichtbare pagina's", + "subpages.index.invisible.help": "Sleep zichtbare pagina's hiernaartoe om ze onzichtbaar te maken.", + "subpages.add.error": "Deze pagina mag geen subpagina's bevatten", + "subpages.add.error.more": "Deze pagina kan niet meer subpagina's bevatten", + "subpages.error.missing": "De pagina kan niet worden gevonden", + "files": "Bestanden", + "files.index.headline": "Bestanden voor", + "files.index.back": "Terug", + "files.index.upload": "Upload een nieuw bestand", + "files.index.upload.first.text": "Deze pagina heeft nog geen bestanden", + "files.index.upload.first.button": "Upload het eerste bestand", + "files.index.edit": "Wijzgen", + "files.index.delete": "Verwijderen", + "files.index.error.disabled": "Deze pagina mag geen bestanden bevatten", + "files.add.error.max": "Het maximum aantal bestanden voor deze pagina is bereikt.", + "files.add.error.extension.missing": "Je kunt geen bestanden uploaden zonder bestandsextensie", + "files.add.error.extension.forbidden": "Bestandsextensie niet toegestaan", + "files.add.error.mime.forbidden": "Mime type niet toegestaan", + "files.add.error.htaccess": "htaccess bestanden kunnen niet geüpload worden", + "files.add.error.invisible": "Onzichtbare bestanden kunnen niet geüpload worden", + "files.add.blueprint.type.error": "Pagina laat alleen het volgende toe:", + "files.add.blueprint.size.error": "Pagina laat alleen bestanden toe met een maximum bestandsgrootte van", + "files.show.name.label": "Bestandsnaam", + "files.show.info.label": "Type / grootte / afmetingen", + "files.show.link.label": "Publieke link", + "files.show.open": "Bekijk/download bestand", + "files.show.back": "Terug", + "files.show.replace": "Vervangen", + "files.show.delete": "Verwijderen", + "files.show.error.rename": "Het bestand kan niet worden hernoemd", + "files.show.error.form": "Vul alle velden correct in", + "files.upload.drop": "Sleep bestanden hiernaartoe...", + "files.upload.click": "... of klik hier om bestanden te uploaden", + "files.replace.drop": "Sleep een bestand hiernaartoe...", + "files.replace.click": "... of klik hier om een bestand te uploaden", + "files.replace.error.type": "Het geüploade bestand moet van hetzelfde type zijn", + "files.delete.headline": "Wil je dit bestand verwijderen?", + "files.error.missing.page": "De pagina kan niet worden gevonden", + "files.error.missing.file": "Het bestand kan niet worden gevonden", + "users": "Users", + "users.index.headline": "Alle gebruikers", + "users.index.add": "Voeg een nieuwe gebruiker toe", + "users.index.edit": "Wijzigen", + "users.index.delete": "Verwijderen", + "users.form.username.label": "Gebruikersnaam", + "users.form.username.placeholder": "Jouw gebruikersnaam", + "users.form.username.help": "Toegestaan: a-z (kleine letters), 0-9 en streepjes (-)", + "users.form.username.readonly": "De gebruikersnaam kan niet worden gewijzigd", + "users.form.firstname.label": "Voornaam", + "users.form.lastname.label": "Achternaam", + "users.form.email.label": "E-mailadres", + "users.form.email.placeholder": "mail@voorbeeld.nl", + "users.form.password.label": "Wachtwoord", + "users.form.password.confirm.label": "Bevestig wachtwoord", + "users.form.password.new.label": "Nieuw wachtwoord", + "users.form.password.new.confirm.label": "Bevestig het nieuwe wachtwoord", + "users.form.password.new.help": "Laat leeg om je huidige wachtwoord te behouden", + "users.form.language.label": "Taal", + "users.form.role.label": "Rol", + "users.form.options.headline": "Account-opties", + "users.form.options.message": "E-mail versturen", + "users.form.options.delete": "Verwijder account", + "users.form.avatar.headline": "Avatar", + "users.form.avatar.upload": "Avatar uploaden", + "users.form.avatar.replace": "Avatar vervangen", + "users.form.avatar.delete": "Delete avatar", + "users.form.back": "Terug naar gebruikers", + "users.form.error.password.confirm": "Bevestig je wachtwoord", + "users.form.error.update": "De gebruiker kan niet worden gewijzigd", + "users.form.error.update.rights": "Je hebt niet voldoende rechten om deze gebruiker te wijzigen", + "users.form.error.create": "De gebruiker kan niet worden aangemaakt", + "users.form.error.permissions.title": "De 'accounts'-map heeft niet voldoende rechten.", + "users.form.error.permissions.text": "Zorg ervoor dat de map 'site/accounts' bestaat en schrijfrechten heeft.", + "users.delete.headline": "Wil je deze gebruiker verwijderen?", + "users.delete.error": "De gebruiker kan niet worden verwijderd", + "users.delete.error.permission": "Je hebt niet voldoende rechten om gebruikers te verwijderen", + "users.delete.error.permission.single": "Je hebt niet voldoende rechten om deze gebruiker te verwijderen", + "users.delete.error.lastadmin": "Je kan de laatste admin niet verwijderen", + "users.avatar.drop": "Sleep een profielfoto hiernaartoe...", + "users.avatar.click": "... of klik hier om afbeelding te selecteren", + "users.avatar.error.type": "Je kunt alleen .jpg, .png en .gif bestanden uploaden", + "users.avatar.error.folder.headline": "De avatar-map heeft geen schrijfrechten.", + "users.avatar.error.folder.text": "Zorg ervoor dat de map /assets/avatars bestaat en schrijfrechten heeft om avatars te uploaden.", + "users.avatar.error.permission": "Je hebt niet voldoende rechtsen om deze avatar te wijzigen", + "users.avatar.delete.error": "De avatar kan niet worden verwijderd", + "users.avatar.delete.error.permission": "Je hebt niet voldoende rechten om de avatar van deze gebruiker te verwijderen", + "users.avatar.delete.success": "De avatar is verwijderd", + "users.avatar.missing": "Deze gebruiker heeft geen avatar", + "users.error.missing": "De gebruiker kan niet worden gevonden", + "user.error.lastadmin": "Jij bent de enige admin. Dit kan niet gewijzigd worden.", + "form.error.missing": "Het formulier kon niet gevonden worden.", + "form.construct.error.invalid": "Ongeldige formulier construction method", + "fields.required": "Verplicht", + "fields.date.label": "Datum", + "fields.date.months": [ + "januari", + "februari", + "maart", + "april", + "mei", + "juni", + "juli", + "augustus", + "september", + "oktober", + "november", + "december" + ], + "fields.date.weekdays": [ + "Zondag", + "Maandag", + "Dinsdag", + "Woensdag", + "Donderdag", + "Vrijdag", + "Zaterdag" + ], + "fields.date.weekdays.short": [ + "Zo", + "Ma", + "Di", + "Wo", + "Do", + "Vr", + "Za" + ], + "fields.email.label": "E-mailadres", + "fields.email.placeholder": "mail@voorbeeld.nl", + "fields.number.label": "Nummer", + "fields.number.placeholder": "#", + "fields.page.label": "Pagina", + "fields.page.placeholder": "pad/naar/pagina", + "fields.password.label": "Wachtwoord", + "fields.structure.add": "Toevoegen", + "fields.structure.add.first": "Voeg het eerste item toe", + "fields.structure.empty": "Nog geen items.", + "fields.structure.entry.error": "Item kon niet gevonden worden", + "fields.structure.cancel": "Annuleren", + "fields.structure.save": "Ok", + "fields.structure.edit": "Wijzigen", + "fields.structure.delete": "Verwijderen", + "fields.structure.delete.label": "Wil je deze entry verwijderen?", + "fields.tags.label": "Tags", + "fields.tel.label": "Telefoon", + "fields.textarea.buttons.bold.label": "Dikgedrukte tekst", + "fields.textarea.buttons.bold.text": "Dikgedrukte tekst", + "fields.textarea.buttons.italic.label": "Cursieve tekst", + "fields.textarea.buttons.italic.text": "Cursieve tekst", + "fields.textarea.buttons.link.label": "Link", + "fields.textarea.buttons.email.label": "E-mailadres", + "fields.textarea.buttons.image.label": "Afbeelding", + "fields.textarea.buttons.file.label": "Bestand", + "fields.toggle.yes": "Ja", + "fields.toggle.no": "Nee", + "fields.toggle.on": "Aan", + "fields.toggle.off": "Uit", + "fields.error.missing.controller": "De 'field controller' file ontbreekt", + "fields.error.missing.class": "De 'field controller' class ontbreekt", + "fields.error.route.invalid": "Ongeldige field route", + "fields.error.extended": "Het veld kan niet worden uitgebreid", + "editor.link.url.label": "URL invoegen", + "editor.link.text.label": "Link-tekst", + "editor.link.text.help": "De link-tekst is niet verplicht", + "editor.email.address.label": "E-mailadres invoegen", + "editor.email.address.placeholder": "mail@voorbeeld.nl", + "editor.email.text.label": "Link-tekst", + "editor.email.text.help": "De link-tekst is niet verplicht", + "editor.file.empty": "Deze pagina heeft geen bestanden", + "editor.image.empty": "Deze pagina heeft geen afbeeldingen", + "autocomplete.method.error": "Ongeldige autocomplete methode", + "blueprints.error.default.missing": "Default blueprint ontbreekt", + "error": "Foutmelding", + "error.headline": "Foutmelding" +} \ No newline at end of file diff --git a/panel/app/translations/nl/package.json b/panel/app/translations/nl/package.json new file mode 100644 index 0000000..2b99314 --- /dev/null +++ b/panel/app/translations/nl/package.json @@ -0,0 +1,4 @@ +{ + "title": "Nederlands", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/pl/core.json b/panel/app/translations/pl/core.json new file mode 100644 index 0000000..804a70b --- /dev/null +++ b/panel/app/translations/pl/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Anuluj", + "add": "Dodaj", + "addit": "Dodaj i Edytuj", + "save": "Zapisz", + "saved": "Zapisano!", + "change": "Zmień", + "delete": "Usuń", + "insert": "Wstaw", + "ok": "Ok", + "routes.error.invalid": "Niewłaściwy adres URL panelu", + "controller.error.invalid": "Niewłaściwy kontroler", + "controller.error.action": "Niewłaściwa akcja", + "view.error.invalid": "Niewłaściwy widok:", + "options.show": "Pokaż opcje", + "options.hide": "Ukryj opcje", + "installation": "Instalacja", + "installation.check.headline": "Instalacja panelu Kirby", + "installation.check.text": "Kirby napotkał następujące problemy podczas instalacji…", + "installation.check.retry": "Ponów próbę", + "installation.check.error": "Wystąpiły jakieś problemy!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts nie ma praw do zapisu", + "installation.check.error.avatars": "/assets/avatars nie ma praw do zapisu", + "installation.check.error.blueprints": "Proszę dodać folder /site/blueprints", + "installation.check.error.content": "Folder content oraz wszystkie foldery i pliki wewnątrz muszą mieć ustawione prawa do zapisu.", + "installation.check.error.thumbs": "Folder thumbs musi mieć ustawione prawa do zapisu.", + "installation.signup.username.label": "Utwórz swoje pierwsze konto.", + "installation.signup.username.placeholder": "Nazwa użytkownika", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@example.com", + "installation.signup.password.label": "Hasło", + "installation.signup.language.label": "Język", + "installation.signup.button": "Utwórz konto", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Nazwa użytkownika", + "login.password.label": "Hasło", + "login.error": "Niepoprawna nazwa użytkownika lub hasło", + "login.button": "Log in", + "login.log.error.permissions": "Plik dziennika logowania nie ma ustawionych praw do zapisu.", + "logout": "Log out", + "topbar.error.class.definition": "Missing topbar definition for class:", + "dashboard": "Panel administracyjny", + "dashboard.index.pages.title": "Strony", + "dashboard.index.pages.edit": "Edytuj", + "dashboard.index.pages.add": "Dodaj", + "dashboard.index.site.title": "Adres URL Twojej strony", + "dashboard.index.account.title": "Twoje konto", + "dashboard.index.account.edit": "Edytuj", + "dashboard.index.metatags.title": "Metadane strony", + "dashboard.index.metatags.edit": "Edytuj", + "dashboard.index.history.title": "Twoje ostatnie modyfikacje", + "dashboard.index.history.text": "Twoje ostatnio zmodyfikowane strony będą wyświetlane tutaj, aby łatwo je było odnaleźć ponownie później.", + "dashboard.index.license.title": "Licencja Kirby", + "dashboard.index.license.text": "Wygląda na to, że korzystasz z Kirby na publicznym serwerze bez ważnej licencji!\nProszę, wesprzyj Kirby i (link: {buy} text: kup licencję teraz).\nJeżeli już posiadasz klucz licencyjny, po prostu dodaj go do pliku konfiguracyjnego: (link: {docs} text: site/config/config.php)", + "metatags": "Ustawienia serwisu", + "metatags.info": "Informacje nt. Kirby", + "metatags.license": "Licencja Kirby", + "metatags.version.toolkit": "Wersja toolkitu", + "metatags.version.kirby": "Wersja Kirby", + "metatags.version.panel": "Wersja panelu", + "metatags.back": "Powróć do panelu administracyjnego", + "metatags.files": "Pliki strony", + "site.delete.error": "Strona nie może zostać usunięta", + "pages.show.settings": "Ustawienia strony", + "pages.show.preview": "Otwórz podgląd", + "pages.show.template": "Szablon", + "pages.show.changeurl": "Zmień URL", + "pages.show.invisible": "Status: niewidzialna", + "pages.show.visible": "Status: widoczna", + "pages.show.changes.text": "Masz niezapisane zmiany!", + "pages.show.changes.button": "Odrzuć", + "pages.show.delete": "Usuń tę stronę", + "pages.show.subpages.title": "Strony", + "pages.show.subpages.edit": "Edytuj", + "pages.show.subpages.add": "Dodaj", + "pages.show.subpages.empty": "Ta strona nie posiada podstron", + "pages.show.files.title": "Pliki", + "pages.show.files.edit": "Edytuj", + "pages.show.files.add": "Dodaj", + "pages.show.files.empty": "Ta strona nie posiada plików", + "pages.show.error.permissions.title": "Nie ma uprawnień do zapisu dla tej strony", + "pages.show.error.permissions.text": "Sprawdź uprawnienia dla folderu content i plików znajdujących się w tym folderze.", + "pages.show.error.permissions.retry": "Spróbuj ponownie", + "pages.show.error.notitle.title": "Szablon tej strony nie posiada pola tytułu", + "pages.show.error.notitle.text": "Dodaj pole tytułu i spróbuj ponownie", + "pages.show.error.notitle.retry": "Spróbuj ponownie", + "pages.show.error.form": "Wypełnij wszystkie pola poprawnie", + "pages.add.title.label": "Dodaj nową stronę", + "pages.add.title.placeholder": "Tytuł", + "pages.add.url.label": "apendyks URL", + "pages.add.url.enter": "(wprowadź tytuł)", + "pages.add.url.close": "Zamknij", + "pages.add.url.help": "Formatowanie: małe litery a-z, 0-9 i myślnik", + "pages.add.template.label": "Szablon", + "pages.add.error.create": "Strona nie może zostać utworzona", + "pages.add.error.title": "Brakuje tytułu", + "pages.add.error.template": "Brakuje szablonu", + "pages.add.error.max.headline": "Nie można dodać nowej strony", + "pages.add.error.max.text": "Maksymalna liczba podstron dla tej strony została już osiągnięta.", + "pages.url.uid.label": "apendyks URL", + "pages.url.uid.label.option": "Utwórz na podstawie tytułu", + "pages.url.error.exists": "Strona z takim apendyksem już istnieje", + "pages.url.error.move": "Apendyks nie mógł zostać zmieniony", + "pages.url.error.rights": "Nie możesz zmienić adres URL tej strony", + "pages.template.select.label": "Szablon", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Pozycja", + "pages.toggle.invisible": "niewidoczny", + "pages.toggle.publish": "Czy na pewno chcesz zmienić status strony na **widoczna**", + "pages.toggle.hide": "Czy na pewno chcesz zmienić status strony na **niewidzialna**", + "pages.toggle.error.error": "Status dla strony błędu nie może zostać zmieniony", + "pages.delete.headline": "Czy na pewno chcesz usunąć tę stronę?", + "pages.delete.error.home.headline": "Strona główna nie może zostać usunięta", + "pages.delete.error.home.text": "Próbujesz skasować stronę główną. To nie jest możliwe i prowadziłoby do niepożądanych skutków.", + "pages.delete.error.error.headline": "Strona błędu nie może zostać usunięta", + "pages.delete.error.error.text": "Próbujesz skasować stronę błędu. To nie jest możliwe i prowadziłoby do niepożądanych skutków.", + "pages.delete.error.children.headline": "Ta strona nie może zostać usunięta", + "pages.delete.error.children.text": "Ta strona posiada podstrony i nie może zostać skasowana. Usuń najpierw wszystkie podstrony.", + "pages.delete.error.blocked.headline": "Ta strona nie może zostać usunięta", + "pages.delete.error.blocked.text": "Ta strona jest zablokowana i nie może zostać usunięta.", + "pages.search.help": "Przeszukaj strony według URL. Nawiguj po wynikach wyszukiwania strzałkami góra/dół i naciśnij enter, by przejść do wybranej strony.", + "pages.search.noresults": "Brak wyników wyszukiwania dla zapytania. Spróbuj ponownie z innym adresem URL.", + "pages.error.missing": "Strona nie została odnaleziona", + "subpages": "Strony", + "subpages.index.headline": "Strony w", + "subpages.index.back": "Wróć", + "subpages.index.add": "Dodaj nową stronę", + "subpages.index.add.first.text": "Ta strona nie posiada jeszcze żadnych podstron", + "subpages.index.add.first.button": "Dodaj pierwszą stronę", + "subpages.index.visible": "Widzialne strony", + "subpages.index.visible.help": "Przeciągnij tutaj niewidzialne strony, by zmienić kolejność/sprawić, żeby były widzialne.", + "subpages.index.invisible": "Niewidzialne strony", + "subpages.index.invisible.help": "Przeciągnij tutaj widzialne strony, aby sprawić, żeby były niewidzialne.", + "subpages.add.error": "Ta strona nie może mieć podstron", + "subpages.add.error.more": "Ta strona nie może mieć więcej podstron", + "subpages.error.missing": "Strona nie została odnaleziona", + "files": "Pliki", + "files.index.headline": "Pliki dla", + "files.index.back": "Wróć", + "files.index.upload": "Dodaj nowy plik", + "files.index.upload.first.text": "Ta strona nie posiada jeszcze żadnych plików", + "files.index.upload.first.button": "Dodaj pierwszy plik", + "files.index.edit": "Edytuj", + "files.index.delete": "Usuń", + "files.index.error.disabled": "Strona nie może mieć żadnych plików", + "files.add.error.max": "Maksymalna liczba plików dla bieżącej stronie został osiągnięty.", + "files.add.error.extension.missing": "Nie można przesyłać plików bez rozszerzenia", + "files.add.error.extension.forbidden": "Zabronione rozszerzenie pliku", + "files.add.error.mime.forbidden": "Zabroniony typ MIME", + "files.add.error.htaccess": "Plik htaccess nie może być przesłany", + "files.add.error.invisible": "Pliki ukryte nie mogą być przesłane", + "files.add.blueprint.type.error": "Strona zezwala tylko na:", + "files.add.blueprint.size.error": "Strona zezwala tylko na pliki o rozmiarze", + "files.show.name.label": "Nazwa pliku", + "files.show.info.label": "Typ / Rozmiar / Wymiary", + "files.show.link.label": "Publiczny link", + "files.show.open": "Pokaż/pobierz plik", + "files.show.back": "Wróć", + "files.show.replace": "Zamień", + "files.show.delete": "Usuń", + "files.show.error.rename": "Nazwa pliku nie mogła zostać zmieniona", + "files.show.error.form": "Wypełnij poprawnie wszystkie pola", + "files.upload.drop": "Upuść pliki tutaj", + "files.upload.click": "…lub kliknij, aby załadować", + "files.replace.drop": "Upuść plik tutaj…", + "files.replace.click": "…lub kliknij, aby zastąpić", + "files.replace.error.type": "Przesłany plik musi być plikiem tego samego typu", + "files.delete.headline": "Czy na pewno chcesz usunąć ten plik?", + "files.error.missing.page": "Strona nie została odnaleziona", + "files.error.missing.file": "Plik nie został odnaleziony", + "users": "Użytkownicy", + "users.index.headline": "Wszyscy użytkownicy", + "users.index.add": "Dodaj nowego użytkownika", + "users.index.edit": "Edytuj", + "users.index.delete": "Usuń", + "users.form.username.label": "Nazwa użytkownika", + "users.form.username.placeholder": "Twoja nazwa użytkownika", + "users.form.username.help": "Dozwolone znaki: małe litery a-z, 0-9 i myślnik", + "users.form.username.readonly": "Nazwa użytkownika nie może zostać zmieniona", + "users.form.firstname.label": "Imię", + "users.form.lastname.label": "Nazwisko", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@example.com", + "users.form.password.label": "Hasło", + "users.form.password.confirm.label": "Potwierdź hasło", + "users.form.password.new.label": "Nowe hasło", + "users.form.password.new.confirm.label": "Potwierdź nowe hasło", + "users.form.password.new.help": "Pozostaw puste, aby zachować bieżące hasło", + "users.form.language.label": "Język", + "users.form.role.label": "Rola", + "users.form.options.headline": "Opcje konta", + "users.form.options.message": "Wyślij email", + "users.form.options.delete": "Usuń konto", + "users.form.avatar.headline": "Zdjęcie profilowe", + "users.form.avatar.upload": "Dodaj zdjęcie profilowe", + "users.form.avatar.replace": "Zmień zdjęcie profilowe", + "users.form.avatar.delete": "Usuń zdjęcie profilowe", + "users.form.back": "Wróć do użytkowników", + "users.form.error.password.confirm": "Potwierdź hasło", + "users.form.error.update": "Użytkownik nie mógł zostać zaktualizowany", + "users.form.error.update.rights": "Nie masz uprawnień do aktualizacji tego użytkownika", + "users.form.error.create": "Użytkownik nie mógł zostać utworzony", + "users.form.error.permissions.title": "Folder account nie ma praw do zapisu", + "users.form.error.permissions.text": "Upewnij się, że folder /site/accounts istnieje i posiada prawa do zapisu.", + "users.delete.headline": "Czy na pewno chcesz usunąć tego użytkownika?", + "users.delete.error": "Użytkownik nie mógł zostać usunięty", + "users.delete.error.permission": "Nie masz uprawnień do usuwania użytkowników", + "users.delete.error.permission.single": "Nie masz uprawnień do usunięcia tego użytkownika", + "users.delete.error.lastadmin": "Nie możesz usunąć jedynego administratora", + "users.avatar.drop": "Upuść zdjęcie profilowe tutaj…", + "users.avatar.click": "…lub kliknij, aby załadować", + "users.avatar.error.type": "Możesz wgrać tylko pliki JPG, PNG i GIF", + "users.avatar.error.folder.headline": "Folder avatar nie ma praw do zapisu", + "users.avatar.error.folder.text": "Utwórz folder /assets/avatars i nadaj mu prawa do zapisu, by móc dodawać zdjęcia profilowe.", + "users.avatar.error.permission": "Nie masz uprawnień, aby zmienić awatar", + "users.avatar.delete.error": "Zdjęcie profilowe nie mogło zostać usunięte", + "users.avatar.delete.error.permission": "Nie masz uprawnień, aby usunąć awatar użytkownika", + "users.avatar.delete.success": "Zdjęcie profilowe zostało usunięte", + "users.avatar.missing": "Ten użytkownik nie ma awataru", + "users.error.missing": "Użytkownik nie został odnaleziony", + "user.error.lastadmin": "Jesteś jedynym administratorem. To ustawienie nie może zostać zmienione.", + "form.error.missing": "Formularz nie może zostać odnaleziony", + "form.construct.error.invalid": "Nieprawidłowa metoda budowy formularza", + "fields.required": "Wymagane", + "fields.date.label": "Data", + "fields.date.months": [ + "Styczeń", + "Luty", + "Marzec", + "Kwiecień", + "Maj", + "Czerwiec", + "Lipiec", + "Sierpień", + "Wrzesień", + "Październik", + "Listopad", + "Grudzień" + ], + "fields.date.weekdays": [ + "Niedziela", + "Poniedziałek", + "Wtorek", + "Środa", + "Czwartek", + "Piątek", + "Sobota" + ], + "fields.date.weekdays.short": [ + "Nd", + "Pn", + "Wt", + "Śr", + "Czw", + "Pt", + "Sb" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@example.com", + "fields.number.label": "Numer", + "fields.number.placeholder": "nr", + "fields.page.label": "Strona", + "fields.page.placeholder": "ścieżka/do/strony", + "fields.password.label": "Hasło", + "fields.structure.add": "Dodaj", + "fields.structure.add.first": "Dodaj pierwszy wpis", + "fields.structure.empty": "Nie ma jeszcze żadnych wpisów.", + "fields.structure.entry.error": "Element nie mógł zostać odnaleziony", + "fields.structure.cancel": "Anuluj", + "fields.structure.save": "Zapisz", + "fields.structure.edit": "Edytuj", + "fields.structure.delete": "Usuń", + "fields.structure.delete.label": "Czy na pewno chcesz usunąć ten wpis?", + "fields.tags.label": "Tagi", + "fields.tel.label": "Telefon", + "fields.textarea.buttons.bold.label": "Pogrubiony tekst", + "fields.textarea.buttons.bold.text": "Pogrubiony tekst", + "fields.textarea.buttons.italic.label": "Kursywa", + "fields.textarea.buttons.italic.text": "Kursywa", + "fields.textarea.buttons.link.label": "Link", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Obrazek", + "fields.textarea.buttons.file.label": "Plik", + "fields.toggle.yes": "Tak", + "fields.toggle.no": "Nie", + "fields.toggle.on": "Włącz", + "fields.toggle.off": "Wyłącz", + "fields.error.missing.controller": "Brak pliku field controller", + "fields.error.missing.class": "Brak klasy field controller", + "fields.error.route.invalid": "Invalid field route", + "fields.error.extended": "Pole nie może zostać rozszerzone", + "editor.link.url.label": "Wstaw URL", + "editor.link.text.label": "Tekst linku", + "editor.link.text.help": "Tekst linku jest opcjonalny", + "editor.email.address.label": "Wstaw adres email", + "editor.email.address.placeholder": "mail@example.com", + "editor.email.text.label": "Tekst linku", + "editor.email.text.help": "Tekst linku jest opcjonalny", + "editor.file.empty": "Ta strona nie ma plików", + "editor.image.empty": "Ta strona nie ma obrazków", + "autocomplete.method.error": "Nieprawidłowa metoda autouzupełniania", + "blueprints.error.default.missing": "Brak domyślnego szablonu", + "error": "Błąd", + "error.headline": "Błąd" +} \ No newline at end of file diff --git a/panel/app/translations/pl/package.json b/panel/app/translations/pl/package.json new file mode 100644 index 0000000..d30e51f --- /dev/null +++ b/panel/app/translations/pl/package.json @@ -0,0 +1,4 @@ +{ + "title": "Polski", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/pt_BR/core.json b/panel/app/translations/pt_BR/core.json new file mode 100644 index 0000000..e674f45 --- /dev/null +++ b/panel/app/translations/pt_BR/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Cancelar", + "add": "Adicionar", + "addit": "Adicionar & Editar", + "save": "Salvar", + "saved": "Salvo!", + "change": "Alterar", + "delete": "Deletar", + "insert": "Inserir", + "ok": "Ok", + "routes.error.invalid": "URL do Painel inválida", + "controller.error.invalid": "Controller inválido", + "controller.error.action": "Ação inválida", + "view.error.invalid": "View inválida: ", + "options.show": "Exibir opções", + "options.hide": "Ocultar opções", + "installation": "Instalação", + "installation.check.headline": "Instalação do Painel Kirby", + "installation.check.text": "Kirby encontrou os seguintes problemas durante a instalação…", + "installation.check.retry": "Tentar novamente", + "installation.check.error": "Temos problemas!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts não tem permissão de escrita", + "installation.check.error.avatars": "/assets/avatars não tem permissão de escrita", + "installation.check.error.blueprints": "Por favor, adicione uma pasta /site/blueprints", + "installation.check.error.content": "A pasta \"content\" e todas subpastas e arquivos devem ter permissão de escrita.", + "installation.check.error.thumbs": "A pasta \"thumbs\" deve ter permissão de escrita.", + "installation.signup.username.label": "Crie sua primeira conta", + "installation.signup.username.placeholder": "Usuário", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@exemplo.com", + "installation.signup.password.label": "Senha", + "installation.signup.language.label": "Idioma", + "installation.signup.button": "Crie sua conta", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Usuário", + "login.password.label": "Senha", + "login.error": "Usuário ou senha inválido", + "login.button": "Log in", + "login.log.error.permissions": "O arquivo de log do login não tem permissão de escrita ", + "logout": "Log out", + "topbar.error.class.definition": "Definição \"topbar\" não existe para classe: ", + "dashboard": "Painel", + "dashboard.index.pages.title": "Páginas", + "dashboard.index.pages.edit": "Editar", + "dashboard.index.pages.add": "Adicionar", + "dashboard.index.site.title": "URL do seu site", + "dashboard.index.account.title": "Sua conta", + "dashboard.index.account.edit": "Editar", + "dashboard.index.metatags.title": "Variáveis do site", + "dashboard.index.metatags.edit": "Editar", + "dashboard.index.history.title": "Atualizações recentes", + "dashboard.index.history.text": "Últimas páginas modificadas serão exibidas aquí para facilitar seu acesso futuro.", + "dashboard.index.license.title": "Licença do Kirby", + "dashboard.index.license.text": "Parece que você está rodando o Kirby em um servidor público sem uma licença válida!\n\nPor favor, apoie o Kirby (link: {buy} text: comprando uma licença agora)\n\nCaso possua uma licença, basta adiciona-la ao seu arquivo config: (link: {docs} text: site/config/config.php)", + "metatags": "Variáveis do site", + "metatags.info": "Informações do Kirby", + "metatags.license": "Licença do Kirby ", + "metatags.version.toolkit": "Versão do Toolkit", + "metatags.version.kirby": "Versão do Kirby", + "metatags.version.panel": "Versão do Painel", + "metatags.back": "Voltar ao Painel", + "metatags.files": "Arquivos do site", + "site.delete.error": "Este site não pode ser deletado", + "pages.show.settings": "Configurações de página", + "pages.show.preview": "Abrir preview", + "pages.show.template": "Template", + "pages.show.changeurl": "Mudar URL", + "pages.show.invisible": "Status: invisível", + "pages.show.visible": "Status: visível", + "pages.show.changes.text": "Você possui alterações não salvas!", + "pages.show.changes.button": "Descartar", + "pages.show.delete": "Deletar esta página", + "pages.show.subpages.title": "Páginas", + "pages.show.subpages.edit": "Editar", + "pages.show.subpages.add": "Adicionar", + "pages.show.subpages.empty": "Esta página não contém subpáginas", + "pages.show.files.title": "Arquivos", + "pages.show.files.edit": "Editar", + "pages.show.files.add": "Adicionar", + "pages.show.files.empty": "Esta página não contém arquivos", + "pages.show.error.permissions.title": "Esta página não tem permissão de escrita", + "pages.show.error.permissions.text": "Favor conferir as permissões da pasta \"content\" e todos seus arquivos", + "pages.show.error.permissions.retry": "Tentar novamente", + "pages.show.error.notitle.title": "O blueprint não possui um campo \"title\"", + "pages.show.error.notitle.text": "Favor adicionar um campo \"title\" e tentar novamente", + "pages.show.error.notitle.retry": "Tentar novamente", + "pages.show.error.form": "Favor preencher todos campos corretamente", + "pages.add.title.label": "Adicionar página", + "pages.add.title.placeholder": "Título", + "pages.add.url.label": "URL-apêndice", + "pages.add.url.enter": "(digite um título)", + "pages.add.url.close": "Fechar", + "pages.add.url.help": "Formato: a-z minúsculas, 0-9 e hífens", + "pages.add.template.label": "Template", + "pages.add.error.create": "A página não pode ser criada", + "pages.add.error.title": "Falta o título", + "pages.add.error.template": "Falta o template", + "pages.add.error.max.headline": "Não se permite novas páginas", + "pages.add.error.max.text": "Número máximo de subpáginas para a página atual foi atingido.", + "pages.url.uid.label": "URL-apêndice", + "pages.url.uid.label.option": "Criar a partir do título", + "pages.url.error.exists": "Uma página com mesmo apêndice já existe", + "pages.url.error.move": "O apêndice não pôde ser alterado", + "pages.url.error.rights": "Você não pode alterar a URL desta página", + "pages.template.select.label": "Template", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Posição", + "pages.toggle.invisible": "Invisível", + "pages.toggle.publish": "Quer mesmo mudar o status desta página para **visível?**", + "pages.toggle.hide": "Quer mesmo mudar o status desta página para **invisível?**", + "pages.toggle.error.error": "O status da página de erro não pode ser alterado", + "pages.delete.headline": "Quer mesmo deletar esta página?", + "pages.delete.error.home.headline": "A página \"home\" não pode ser deletada", + "pages.delete.error.home.text": "Você está tentando deletar a página \"home\". O que não é possível e podería causar efeitos indesejados.", + "pages.delete.error.error.headline": "A página \"error\" não pode ser deletada", + "pages.delete.error.error.text": "Você está tentando deletar a página \"error\". O que não é possível e podería causar efeitos indesejados.", + "pages.delete.error.children.headline": "A página não pode ser deletada", + "pages.delete.error.children.text": "Esta página contém subpáginas e não pode ser deletada. Favor deletar todas subpáginas antes.", + "pages.delete.error.blocked.headline": "A página não pode ser deletada", + "pages.delete.error.blocked.text": "A página está bloqueada e não pode ser deletada.", + "pages.search.help": "Procurar páginas por URL. Navegue pelos resultados usando as teclas \"acima\" e \"abaixo\" e pressione \"enter\" para saltar à página selecionada.", + "pages.search.noresults": "Busca sem resultados. Favor tentar novamente com outra URL.", + "pages.error.missing": "Página não encontrada", + "subpages": "Páginas", + "subpages.index.headline": "Páginas em", + "subpages.index.back": "Voltar", + "subpages.index.add": "Adicionar nova página", + "subpages.index.add.first.text": "Página ainda sem subpáginas", + "subpages.index.add.first.button": "Adicione a primeira página", + "subpages.index.visible": "Páginas visíveis", + "subpages.index.visible.help": "Arraste as páginas invisíveis aquí para ordenar/torna-las visíveis.", + "subpages.index.invisible": "Páginas invisíveis", + "subpages.index.invisible.help": "Arraste as páginas visíveis aquí para desordenar/torna-las invisíveis", + "subpages.add.error": "Esta página não pode ter sub-páginas ", + "subpages.add.error.more": "Esta página não pode ter mais sub-páginas ", + "subpages.error.missing": "Página não encontrada", + "files": "Arquivos", + "files.index.headline": "Arquivos para", + "files.index.back": "Voltar", + "files.index.upload": "Subir novo arquivo", + "files.index.upload.first.text": "Página ainda sem arquivos", + "files.index.upload.first.button": "Suba o primeiro arquivo", + "files.index.edit": "Editar", + "files.index.delete": "Deletar", + "files.index.error.disabled": "Esta página não pode ter nenhum arquivo", + "files.add.error.max": "O numero máximo de arquivos desta página foi atingido.", + "files.add.error.extension.missing": "Você não pode subir arquivos sem extensão ", + "files.add.error.extension.forbidden": "Extensão de arquivo não permitida", + "files.add.error.mime.forbidden": "\"mime type\" não permitido", + "files.add.error.htaccess": "Arquivos \"htaccess\" não podem ser subidos", + "files.add.error.invisible": "Arquivos invisíveis não podem ser subidos", + "files.add.blueprint.type.error": "Esta página permite apenas: ", + "files.add.blueprint.size.error": "Esta página permite arquivos com tamanho de ", + "files.show.name.label": "Nome do arquivo", + "files.show.info.label": "Tipo / Tamanho / Dimensões", + "files.show.link.label": "Link público", + "files.show.open": "Exibir/baixar arquivo", + "files.show.back": "Voltar", + "files.show.replace": "Substituir", + "files.show.delete": "Deletar", + "files.show.error.rename": "O arquivo não pode ser renomeado", + "files.show.error.form": "Favor preencher todos campos corretamente", + "files.upload.drop": "Arraste os arquivos aquí…", + "files.upload.click": "…ou clique para subir", + "files.replace.drop": "Arraste o arquivo aquí…", + "files.replace.click": "…ou clique para substituir", + "files.replace.error.type": "O arquivo subido deve ser do mesmo tipo", + "files.delete.headline": "Quer mesmo deletar este arquivo?", + "files.error.missing.page": "Página não encontrada", + "files.error.missing.file": "Arquivo não encontrado", + "users": "Usuários", + "users.index.headline": "Todos usuários", + "users.index.add": "Adicionar novo usuário", + "users.index.edit": "Editar", + "users.index.delete": "Deletar", + "users.form.username.label": "Usuário", + "users.form.username.placeholder": "Seu nome de usuário", + "users.form.username.help": "Caracteres permitidos: a-z mininúsculas, 0-9 e hífens", + "users.form.username.readonly": "O nome de usuário nao pode ser alterado", + "users.form.firstname.label": "Primeiro nome", + "users.form.lastname.label": "Último nome", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@exemplo.com", + "users.form.password.label": "Senha", + "users.form.password.confirm.label": "Confimar senha", + "users.form.password.new.label": "Nova senha", + "users.form.password.new.confirm.label": "Confirmar nova senha", + "users.form.password.new.help": "Deixe em branco para manter a senha atual", + "users.form.language.label": "Idioma", + "users.form.role.label": "Papel", + "users.form.options.headline": "Opções de conta", + "users.form.options.message": "Enviar email", + "users.form.options.delete": "Deletar conta", + "users.form.avatar.headline": "Foto do perfil", + "users.form.avatar.upload": "Subir foto do perfil", + "users.form.avatar.replace": "Substituir foto do perfil", + "users.form.avatar.delete": "Deletar foto do perfil", + "users.form.back": "Voltar para usuários", + "users.form.error.password.confirm": "Favor confirmar a senha", + "users.form.error.update": "O usuário não pode ser atualizado", + "users.form.error.update.rights": "Você não tem permissão para atualizar este usuário", + "users.form.error.create": "O usuário não pode ser criado", + "users.form.error.permissions.title": "A pasta \"account\" não tem permissão de escrita", + "users.form.error.permissions.text": "Favor se certificar que /site/accounts existe e possui permissão de escrita.", + "users.delete.headline": "Quer mesmo deletar este usuário?", + "users.delete.error": "Este usuário nao pode ser deletado", + "users.delete.error.permission": "Você não tem permissão para deletar usuários", + "users.delete.error.permission.single": "Você não tem permissão para deletar este usuário ", + "users.delete.error.lastadmin": "Você não pode deletar o último usuário administrador ", + "users.avatar.drop": "Arraste a foto do perfil aquí…", + "users.avatar.click": "…ou clique para subir", + "users.avatar.error.type": "Você pode subir somente arquivos JPG, PNG and GIF", + "users.avatar.error.folder.headline": "A pasta \"avatar\" não tem permissão de escrita", + "users.avatar.error.folder.text": "Favor criar a pasta /assets/avatars e permita que seja escrita para subir fotos do perfil.", + "users.avatar.error.permission": "Você não pode alterar o avatar", + "users.avatar.delete.error": "A foto do perfil não pode ser deletada", + "users.avatar.delete.error.permission": "Você não tem permissão para deletar o avatar deste usuário", + "users.avatar.delete.success": "A foto do perfil foi deletada", + "users.avatar.missing": "Este usuário não possui avatar", + "users.error.missing": "Usuário não encontrado", + "user.error.lastadmin": "Você é o único administrador. Isto não pode ser alterado.", + "form.error.missing": "O formulário não foi encontrado ", + "form.construct.error.invalid": "Método inválido de construção de formulário ", + "fields.required": "Requerido", + "fields.date.label": "Data", + "fields.date.months": [ + "Janeiro", + "Fevereiro", + "Março", + "Abril", + "Maio", + "Junho", + "Julho", + "Agosto", + "Setembro", + "Outubro", + "Novembro", + "Dezembro" + ], + "fields.date.weekdays": [ + "Domingo", + "Segunda", + "Terça", + "Quarta", + "Quinta", + "Sexta", + "Sábado" + ], + "fields.date.weekdays.short": [ + "Dom", + "Seg", + "Ter", + "Qua", + "Qui", + "Sex", + "Sáb" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@exemplo.com", + "fields.number.label": "Número", + "fields.number.placeholder": "#", + "fields.page.label": "Página", + "fields.page.placeholder": "caminho/da/pagina", + "fields.password.label": "Senha", + "fields.structure.add": "Adicionar", + "fields.structure.add.first": "Adicionar primeira entrada", + "fields.structure.empty": "Sem entradas ainda.", + "fields.structure.entry.error": "Item não encontrado ", + "fields.structure.cancel": "Cancelar", + "fields.structure.save": "Salvar", + "fields.structure.edit": "Editar", + "fields.structure.delete": "Deletar", + "fields.structure.delete.label": "Quer mesmo deletar esta entrada?", + "fields.tags.label": "Tags", + "fields.tel.label": "Fone", + "fields.textarea.buttons.bold.label": "Texto negrito", + "fields.textarea.buttons.bold.text": "Texto negrito", + "fields.textarea.buttons.italic.label": "Texto itálico", + "fields.textarea.buttons.italic.text": "Texto itálico", + "fields.textarea.buttons.link.label": "Link", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Imagem", + "fields.textarea.buttons.file.label": "Arquivo", + "fields.toggle.yes": "Sim", + "fields.toggle.no": "Não", + "fields.toggle.on": "Ligado", + "fields.toggle.off": "Desligado", + "fields.error.missing.controller": "O arquivo do \"field controller\" não foi encontrado ", + "fields.error.missing.class": "A classe do \"field controller\" não foi encontrada ", + "fields.error.route.invalid": "\"field route\" inválida ", + "fields.error.extended": "O \"field\" não pode ser extendido ", + "editor.link.url.label": "Inserir URL", + "editor.link.text.label": "Texto do link", + "editor.link.text.help": "O texto do link é opcional", + "editor.email.address.label": "Inserir endereço de email", + "editor.email.address.placeholder": "mail@exemplo.com", + "editor.email.text.label": "Texto do link", + "editor.email.text.help": "O texto do link é opcional", + "editor.file.empty": "Esta página não tem arquivos", + "editor.image.empty": "Esta página não tem imagens", + "autocomplete.method.error": "Método de auto-completar inválido", + "blueprints.error.default.missing": "Blueprint \"default\" não encontrada ", + "error": "Erro", + "error.headline": "Erro" +} \ No newline at end of file diff --git a/panel/app/translations/pt_BR/package.json b/panel/app/translations/pt_BR/package.json new file mode 100644 index 0000000..885ba0c --- /dev/null +++ b/panel/app/translations/pt_BR/package.json @@ -0,0 +1,4 @@ +{ + "title": "Português (Brasileiro)‎", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/pt_PT/core.json b/panel/app/translations/pt_PT/core.json new file mode 100644 index 0000000..891471d --- /dev/null +++ b/panel/app/translations/pt_PT/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Cancelar", + "add": "Adicionar", + "addit": "Adicionar e editar", + "save": "Gravar", + "saved": "Gravado!", + "change": "Mudar", + "delete": "Apagar", + "insert": "Inserir", + "ok": "Ok", + "routes.error.invalid": "URL de panel inválida", + "controller.error.invalid": "Controlador inválido", + "controller.error.action": "Ação inválida", + "view.error.invalid": "Vista inválida:", + "options.show": "Exibir opções", + "options.hide": "Ocultar opções", + "installation": "Instalação", + "installation.check.headline": "Instalação do Painel Kirby", + "installation.check.text": "Kirby encontrou os seguintes problemas durante a instalação…", + "installation.check.retry": "Tenta novamente", + "installation.check.error": "Temos problemas!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts não tem permissão de escrita", + "installation.check.error.avatars": "/assets/avatars não tem permissão de escrita", + "installation.check.error.blueprints": "Por favor, adiciona uma pasta /site/blueprints", + "installation.check.error.content": "A pasta \"content\" e todas as subpastas e ficheiros devem ter permissão de escrita.", + "installation.check.error.thumbs": "A pasta \"thumbs\" deve ter permissão de escrita.", + "installation.signup.username.label": "Cria a tua primeira conta", + "installation.signup.username.placeholder": "Utilizador", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@exemplo.com", + "installation.signup.password.label": "Palavra-passe", + "installation.signup.language.label": "Idioma", + "installation.signup.button": "Cria a tua conta", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Utilizador", + "login.password.label": "Palavra-passe", + "login.error": "Utilizador ou Palavra-passe inválida", + "login.button": "Log in", + "login.log.error.permissions": "Ficheiro de log do login não é descritível.", + "logout": "Log out", + "topbar.error.class.definition": "Falta a definição topbar da classe:", + "dashboard": "Painel", + "dashboard.index.pages.title": "Páginas", + "dashboard.index.pages.edit": "Editar", + "dashboard.index.pages.add": "Adicionar", + "dashboard.index.site.title": "URL do teu site", + "dashboard.index.account.title": "A tua conta", + "dashboard.index.account.edit": "Editar", + "dashboard.index.metatags.title": "Variáveis do site", + "dashboard.index.metatags.edit": "Editar", + "dashboard.index.history.title": "Atualizações recentes", + "dashboard.index.history.text": "As últimas páginas modificadas por ti serão exibidas aqui para facilitar a sua consulta no futuro.", + "dashboard.index.license.title": "Licença Kirby", + "dashboard.index.license.text": "Aparentemente estas a executar Kirby num servidor público sem licença válida!\n\nPor favor apoia Kirby e (link: {compra} text: adquira agóra uma licença)\n\nSe já adquiriste uma licença, simplesmente adiciona-a ao teu ficheiro de configuração: (link: {docs} text: site/config/config.php)", + "metatags": "Variáveis do site", + "metatags.info": "Informação Kirby", + "metatags.license": "Licença Kirby", + "metatags.version.toolkit": "Versão toolkit", + "metatags.version.kirby": "Versão Kirby", + "metatags.version.panel": "Versão panel", + "metatags.back": "Voltar ao Painel", + "metatags.files": "Ficheiros do site", + "site.delete.error": "A site não pode ser apagada", + "pages.show.settings": "Configurações de página", + "pages.show.preview": "Abrir preview", + "pages.show.template": "Template", + "pages.show.changeurl": "Mudar URL", + "pages.show.invisible": "Estatuto: invisível", + "pages.show.visible": "Estatuto: visível", + "pages.show.changes.text": "Tens alteraçoes ainda não gravadas!", + "pages.show.changes.button": "Descartar", + "pages.show.delete": "Apagar esta página", + "pages.show.subpages.title": "Páginas", + "pages.show.subpages.edit": "Editar", + "pages.show.subpages.add": "Adicionar", + "pages.show.subpages.empty": "Esta página não contém subpáginas", + "pages.show.files.title": "Ficheiros", + "pages.show.files.edit": "Editar", + "pages.show.files.add": "Adicionar", + "pages.show.files.empty": "Esta página não contém ficheiros", + "pages.show.error.permissions.title": "Esta página não tem permissão de escrita", + "pages.show.error.permissions.text": "Por favor confere as permissões da pasta \"content\" e todos seus ficheiros", + "pages.show.error.permissions.retry": "Tentar novamente", + "pages.show.error.notitle.title": "O blueprint não possui um campo \"title\"", + "pages.show.error.notitle.text": "Por favor adiciona um campo \"title\" e tenta novamente", + "pages.show.error.notitle.retry": "Tenta novamente", + "pages.show.error.form": "Por favor preenche todos os campos corretamente", + "pages.add.title.label": "Adicionar página", + "pages.add.title.placeholder": "Título", + "pages.add.url.label": "URL-apêndice", + "pages.add.url.enter": "(introduz um título)", + "pages.add.url.close": "Fechar", + "pages.add.url.help": "Formato: a-z minúsculas, 0-9 e hífens", + "pages.add.template.label": "Template", + "pages.add.error.create": "Não foi possível criar a página", + "pages.add.error.title": "Falta o título", + "pages.add.error.template": "Falta o template", + "pages.add.error.max.headline": "Não são permitidas novas páginas", + "pages.add.error.max.text": "Número máximo de subpáginas para a página atual foi atingido.", + "pages.url.uid.label": "URL-apêndice", + "pages.url.uid.label.option": "Criar a partir do título", + "pages.url.error.exists": "Já existe uma página com mesmo apêndice", + "pages.url.error.move": "O apêndice não pôde ser alterado", + "pages.url.error.rights": "Não é possível mudar o URL desta página", + "pages.template.select.label": "Template", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Posição", + "pages.toggle.invisible": "Invisível", + "pages.toggle.publish": "Queres mesmo alterar o estatuto desta página para **visível**?", + "pages.toggle.hide": "Queres mesmo alterar o estatuto desta página para **invisível**?", + "pages.toggle.error.error": "O estatuto da pagina erro não pode ser alterado", + "pages.delete.headline": "Queres mesmo apagar esta página?", + "pages.delete.error.home.headline": "A página \"home\" não pode ser apagada", + "pages.delete.error.home.text": "Estás a tentar apagar a página \"home\". O que não é possível e poderia causar efeitos indesejados.", + "pages.delete.error.error.headline": "A página \"error\" não pode ser apagada", + "pages.delete.error.error.text": "Estás a tentar apagar a página \"error\". O que não é possível e poderia causar efeitos indesejados.", + "pages.delete.error.children.headline": "A página não pode ser apagada", + "pages.delete.error.children.text": "Esta página contém subpáginas e não pode ser apagada. Por favor apaga todas as subpáginas antes.", + "pages.delete.error.blocked.headline": "A página não pode ser apagada", + "pages.delete.error.blocked.text": "A página está bloqueada e não pode ser apagada.", + "pages.search.help": "Procurar páginas por URL. Navega pelos resultados usando as teclas \"cima\" e \"baixo\" e pressiona \"enter\" para saltar para a página selecionada.", + "pages.search.noresults": "Pesquisa sem resultados. Por favor tenta novamente com outra URL.", + "pages.error.missing": "Página não encontrada", + "subpages": "Páginas", + "subpages.index.headline": "Páginas em", + "subpages.index.back": "Voltar", + "subpages.index.add": "Adicionar nova página", + "subpages.index.add.first.text": "Página ainda sem subpáginas", + "subpages.index.add.first.button": "Adiciona a primeira página", + "subpages.index.visible": "Páginas visíveis", + "subpages.index.visible.help": "Arrasta as páginas invisíveis aqui para ordenar/torna-las visíveis.", + "subpages.index.invisible": "Páginas invisíveis", + "subpages.index.invisible.help": "Arrasta as páginas visíveis aqui para desordenar/torna-las invisíveis", + "subpages.add.error": "Página sem permissão de ter paginas inferiores", + "subpages.add.error.more": "Esta página não pode ter mais paginas inferiores", + "subpages.error.missing": "Página não encontrada", + "files": "Ficheiros", + "files.index.headline": "Ficheiros para", + "files.index.back": "Voltar", + "files.index.upload": "Faz upload dum novo ficheiro", + "files.index.upload.first.text": "Página ainda sem ficheiros", + "files.index.upload.first.button": "Faz upload do primeiro ficheiro", + "files.index.edit": "Editar", + "files.index.delete": "Apagar", + "files.index.error.disabled": "Página sem permissão de ter ficheiros", + "files.add.error.max": "O máximo de ficheiros foi atingido para esta página.", + "files.add.error.extension.missing": "Não podes fazer upload sem extensão", + "files.add.error.extension.forbidden": "Extensão proibida", + "files.add.error.mime.forbidden": "Tipo mime proibido", + "files.add.error.htaccess": "Não se pode fazer upload de ficheiros htaccess", + "files.add.error.invisible": "Não se pode fazer upload de ficheiros invisíveis", + "files.add.blueprint.type.error": "Pagina apenas permite:", + "files.add.blueprint.size.error": "A pagina apenas permite tamanho de", + "files.show.name.label": "Nome do ficheiro", + "files.show.info.label": "Tipo / Tamanho / Dimensões", + "files.show.link.label": "Link público", + "files.show.open": "Exibir/Download ficheiro", + "files.show.back": "Voltar", + "files.show.replace": "Substituir", + "files.show.delete": "Apagar", + "files.show.error.rename": "O ficheiro não pode ser renomeado", + "files.show.error.form": "Por favor preenche os todos campos corretamente", + "files.upload.drop": "Arrasta os ficheiros para aqui…", + "files.upload.click": "…ou clica para fazer upload", + "files.replace.drop": "Arrasta o ficheiros para aqui…", + "files.replace.click": "…ou clica para substituir", + "files.replace.error.type": "O ficheiro que fizeste upload deve ser do mesmo tipo", + "files.delete.headline": "Queres mesmo apagar este ficheiro?", + "files.error.missing.page": "Página não encontrada", + "files.error.missing.file": "Ficheiro não encontrado", + "users": "Utilizadores", + "users.index.headline": "Todos os utilizadores", + "users.index.add": "Adicionar novo utilizador", + "users.index.edit": "Editar", + "users.index.delete": "Apagar", + "users.form.username.label": "Utilizador", + "users.form.username.placeholder": "Nome do utilizador", + "users.form.username.help": "Caracteres permitidos: a-z mininúsculas, 0-9 e hífens", + "users.form.username.readonly": "O nome de utilizador nao pode ser alterado", + "users.form.firstname.label": "Primeiro nome", + "users.form.lastname.label": "Último nome", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@exemplo.com", + "users.form.password.label": "Palavra-passe", + "users.form.password.confirm.label": "Confima a Palavra-passe", + "users.form.password.new.label": "Nova Palavra-passe", + "users.form.password.new.confirm.label": "Confirma a nova Palavra-passe", + "users.form.password.new.help": "Deixa em branco para manter a Palavra-passe atual", + "users.form.language.label": "Idioma", + "users.form.role.label": "Role", + "users.form.options.headline": "Opções de conta", + "users.form.options.message": "Enviar email", + "users.form.options.delete": "Apagar conta", + "users.form.avatar.headline": "Foto do perfil", + "users.form.avatar.upload": "Upload foto do perfil", + "users.form.avatar.replace": "Substituir foto do perfil", + "users.form.avatar.delete": "Apagar foto do perfil", + "users.form.back": "Voltar para utilizadores", + "users.form.error.password.confirm": "Por favor confirmar a Palavra-passe", + "users.form.error.update": "O utilizador não pode ser atualizado", + "users.form.error.update.rights": "Sem autorização para fazeres update a este utilizador", + "users.form.error.create": "O utilizador não pode ser criado", + "users.form.error.permissions.title": "O folder \"account\" não tem permissão de escrita", + "users.form.error.permissions.text": "Por favor certifica-te que /site/accounts existe e possui permissão de escrita.", + "users.delete.headline": "Queres mesmo apagar este utilizador?", + "users.delete.error": "Este utilizador nao pode ser apagado", + "users.delete.error.permission": "Não tens autorização para apagar utilizadores", + "users.delete.error.permission.single": "Não tens autorização para apagar este utilizador", + "users.delete.error.lastadmin": "Não é possivel apagar o ultimo admin", + "users.avatar.drop": "Arrasta a foto do perfil aqui…", + "users.avatar.click": "…ou clica para fazer upload", + "users.avatar.error.type": "Apenas podes fazer upload de ficheiros JPG, PNG and GIF", + "users.avatar.error.folder.headline": "O folder \"avatar\" não tem permissão de escrita", + "users.avatar.error.folder.text": "Por favor cria o folder /assets/avatars e garante que tem permissão de escrita para poder fazer upload de fotos de perfil.", + "users.avatar.error.permission": "Não tens autorização para mudar o avatar", + "users.avatar.delete.error": "A foto do perfil não pode ser apagada", + "users.avatar.delete.error.permission": "Não tens autorização para apagar o avatar deste utilizador", + "users.avatar.delete.success": "A foto do perfil foi apagada", + "users.avatar.missing": "Este utilizador não tem avatar", + "users.error.missing": "Utilizador não encontrado", + "user.error.lastadmin": "Só es admin. Isto não pode ser mudado.", + "form.error.missing": "Não foi possível encontrar o formulário", + "form.construct.error.invalid": "Método inválido de construção do formulário", + "fields.required": "Obrigatório", + "fields.date.label": "Data", + "fields.date.months": [ + "Janeiro", + "Fevereiro", + "Março", + "Abril", + "Maio", + "Junho", + "Julho", + "Agosto", + "Setembro", + "Outubro", + "Novembro", + "Dezembro" + ], + "fields.date.weekdays": [ + "Domingo", + "Segunda", + "Terça", + "Quarta", + "Quinta", + "Sexta", + "Sábado" + ], + "fields.date.weekdays.short": [ + "Dom", + "Seg", + "Ter", + "Qua", + "Qui", + "Sex", + "Sáb" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@exemplo.com", + "fields.number.label": "Número", + "fields.number.placeholder": "#", + "fields.page.label": "Página", + "fields.page.placeholder": "caminho/da/pagina", + "fields.password.label": "Palavra-passe", + "fields.structure.add": "Adicionar", + "fields.structure.add.first": "Adicionar primeira entrada", + "fields.structure.empty": "Sem entradas ainda.", + "fields.structure.entry.error": "O item não foi encontrado", + "fields.structure.cancel": "Cancelar", + "fields.structure.save": "Gravar", + "fields.structure.edit": "Editar", + "fields.structure.delete": "Apagar", + "fields.structure.delete.label": "Queres mesmo apagar esta entrada?", + "fields.tags.label": "Tags", + "fields.tel.label": "Fone", + "fields.textarea.buttons.bold.label": "Texto negrito", + "fields.textarea.buttons.bold.text": "Texto negrito", + "fields.textarea.buttons.italic.label": "Texto itálico", + "fields.textarea.buttons.italic.text": "Texto itálico", + "fields.textarea.buttons.link.label": "Link", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Imagem", + "fields.textarea.buttons.file.label": "Ficheiro", + "fields.toggle.yes": "Sim", + "fields.toggle.no": "Não", + "fields.toggle.on": "Ligado", + "fields.toggle.off": "Desligado", + "fields.error.missing.controller": "Falta o ficheiro do controlador de campo ", + "fields.error.missing.class": "Falta a classe do controlador de campo", + "fields.error.route.invalid": "Rota de campo inválida", + "fields.error.extended": "Não é possível extender o campo", + "editor.link.url.label": "Inserir URL", + "editor.link.text.label": "Texto do link", + "editor.link.text.help": "O texto do link é opcional", + "editor.email.address.label": "Inserir endereço de email", + "editor.email.address.placeholder": "mail@exemplo.com", + "editor.email.text.label": "Texto do link", + "editor.email.text.help": "O texto do link é opcional", + "editor.file.empty": "Esta página não tem ficheiros", + "editor.image.empty": "Esta página não tem imagens", + "autocomplete.method.error": "Método inválido de auto-contemplação", + "blueprints.error.default.missing": "Falta do blueprint padrão", + "error": "Erro", + "error.headline": "Erro" +} \ No newline at end of file diff --git a/panel/app/translations/pt_PT/package.json b/panel/app/translations/pt_PT/package.json new file mode 100644 index 0000000..733fd36 --- /dev/null +++ b/panel/app/translations/pt_PT/package.json @@ -0,0 +1,4 @@ +{ + "title": "Português (Portugal)", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/ro/core.json b/panel/app/translations/ro/core.json new file mode 100644 index 0000000..f783c75 --- /dev/null +++ b/panel/app/translations/ro/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Renunţă", + "add": "Adaugă", + "addit": "Add & Edit", + "save": "Salvează", + "saved": "Salvat!", + "change": "Modifică", + "delete": "Şterge", + "insert": "Inserează", + "ok": "Ok", + "routes.error.invalid": "Invalid Panel URL", + "controller.error.invalid": "Invalid controller", + "controller.error.action": "Invalid action", + "view.error.invalid": "Invalid view:", + "options.show": "Arată opţiunile", + "options.hide": "Ascunde opţiunile", + "installation": "Instalare", + "installation.check.headline": "Instalarea panoului de administrare", + "installation.check.text": "Sistemul a întâmpinat următoarele probleme în timpul instalării:", + "installation.check.retry": "Ignoră", + "installation.check.error": "Sunt câteva probleme!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "Directorul /site/accounts nu are permisiuni de scriere.", + "installation.check.error.avatars": "Directorul /assets/avatars nu are permisiuni de scriere.", + "installation.check.error.blueprints": "Creează te rog directorul /site/blueprints!", + "installation.check.error.content": "Directorul /content şi conţinutul lui trebuie să aibă permisiuni de scriere.", + "installation.check.error.thumbs": "Directorul /thumbs trebuie să aibă permisiuni de scriere.", + "installation.signup.username.label": "Creează primul cont de utilizator!", + "installation.signup.username.placeholder": "Nume utilizator", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "email@exemplu.com", + "installation.signup.password.label": "Parola", + "installation.signup.language.label": "Limba", + "installation.signup.button": "Creează contul", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Nume utilizator", + "login.password.label": "Parola", + "login.error": "Nume de utilizator greşit, sau parolă invalidă.", + "login.button": "Log in", + "login.log.error.permissions": "Login log file is not writable.", + "logout": "Log out", + "topbar.error.class.definition": "Missing topbar definition for class:", + "dashboard": "Panoul de control", + "dashboard.index.pages.title": "Pagini", + "dashboard.index.pages.edit": "Editează", + "dashboard.index.pages.add": "Adaugă", + "dashboard.index.site.title": "Adresa URL a saitului", + "dashboard.index.account.title": "Contul tău", + "dashboard.index.account.edit": "Editează", + "dashboard.index.metatags.title": "Variabile sait", + "dashboard.index.metatags.edit": "Editează", + "dashboard.index.history.title": "Ultimele pagini modificate", + "dashboard.index.history.text": "Paginile modificate recent vor fi afişate aici pentru a putea fi găsite uşor mai târziu.", + "dashboard.index.license.title": "Licenţa Kirby", + "dashboard.index.license.text": "It seems you are running Kirby on a public server without a valid license!\n\nPlease, support Kirby and (link: {buy} text: buy a license now)\n\nIf you already have a license key, just add it to your config file: (link: {docs} text: site/config/config.php)", + "metatags": "Variabile sait", + "metatags.info": "Info Kirby", + "metatags.license": "Licenţa Kirby", + "metatags.version.toolkit": "Versiune Toolkit", + "metatags.version.kirby": "Versiune Kirby", + "metatags.version.panel": "Versiune Panel", + "metatags.back": "Înapoi la panoul de control", + "metatags.files": "Fişiere sait", + "site.delete.error": "The site cannot be deleted", + "pages.show.settings": "Setări pagină", + "pages.show.preview": "Previzualizare", + "pages.show.template": "Tip pagină", + "pages.show.changeurl": "Modifică URL", + "pages.show.invisible": "Status: invizibilă", + "pages.show.visible": "Status: vizibilă", + "pages.show.changes.text": "Există modificări nesalvate!", + "pages.show.changes.button": "Renunţă", + "pages.show.delete": "Şterge această pagină", + "pages.show.subpages.title": "Pagini", + "pages.show.subpages.edit": "Editează", + "pages.show.subpages.add": "Adaugă", + "pages.show.subpages.empty": "Această pagină nu are subpagini.", + "pages.show.files.title": "Fişiere", + "pages.show.files.edit": "Editează", + "pages.show.files.add": "Adaugă", + "pages.show.files.empty": "Această pagină nu conţine fişiere", + "pages.show.error.permissions.title": "Pagina nu are permisiuni de scriere.", + "pages.show.error.permissions.text": "Verifică permisiunile directorului /content si a conţinutului său.", + "pages.show.error.permissions.retry": "Ignoră", + "pages.show.error.notitle.title": "Şablonul nu conţine un câmp pentru titlu.", + "pages.show.error.notitle.text": "Adaugă câmpul pentru titlu în şablon şi incearcă din nou.", + "pages.show.error.notitle.retry": "Ignoră", + "pages.show.error.form": "Completează corect toate câmpurile, te rog!", + "pages.add.title.label": "Adaugă o pagina nouă", + "pages.add.title.placeholder": "Titlu", + "pages.add.url.label": "Terminaţia URL", + "pages.add.url.enter": "(completează titlul)", + "pages.add.url.close": "Închide", + "pages.add.url.help": "Caractere permise: litere mici de la a la z, cifre şi cratime.", + "pages.add.template.label": "Tip pagină", + "pages.add.error.create": "The page could not be created", + "pages.add.error.title": "Lipseşte titlul", + "pages.add.error.template": "Nu a fost selectat tipul paginii!", + "pages.add.error.max.headline": "Nu sunt permise pagini noi!", + "pages.add.error.max.text": "A fost atins numărul maxim de subpagini pentru pagina aceasta.", + "pages.url.uid.label": "Terminaţia URL", + "pages.url.uid.label.option": "Foloseşte titlul", + "pages.url.error.exists": "Există deja o pagină cu aceeaşi terminaţie URL!", + "pages.url.error.move": "Terminaţia URL nu poate fi modificată!", + "pages.url.error.rights": "You cannot change the URL of this page", + "pages.template.select.label": "Tip pagină", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Position", + "pages.toggle.invisible": "invisible", + "pages.toggle.publish": "Sigur vrei sa faci **vizibilă** această pagină?", + "pages.toggle.hide": "Sigur vrei sa faci **invizibilă** această pagină?", + "pages.toggle.error.error": "The status of the error page cannot be changed", + "pages.delete.headline": "Sigur vrei sa ştergi această pagină?", + "pages.delete.error.home.headline": "Pagina principală nu poate fi ştearsă!", + "pages.delete.error.home.text": "Ai încercat să ştergi pagina principală a saitului. Acest lucru nu e posibil pentru că ar apărea efecte nedorite.", + "pages.delete.error.error.headline": "Pagina de eroare nu poate fi ştearsă!", + "pages.delete.error.error.text": "Ai încercat să ştergi pagina de eroare a saitului. Acest lucru nu e posibil pentru că ar apărea efecte nedorite.", + "pages.delete.error.children.headline": "Pagina nu poate fi ştearsă!", + "pages.delete.error.children.text": "Această pagină conţine subpagini şi nu poate fi ştearsă. Şterge te rog subpaginile mai întâi.", + "pages.delete.error.blocked.headline": "Pagina nu poate fi ştearsă!", + "pages.delete.error.blocked.text": "Această pagină este blocată şi nu poate fi ştearsă.", + "pages.search.help": "Caută pagini în funcţie de URL. Navighează printre rezultatele afişate cu ajutorul săgeţilor de pe tastatură şi apasă Enter pentru a accesa pagina căutată.", + "pages.search.noresults": "Nu există rezultate pentru căutarea făcută. Încearcă din nou folosind un URL diferit.", + "pages.error.missing": "Pagina nu poate fi găsită!", + "subpages": "Pagini", + "subpages.index.headline": "Subpagini", + "subpages.index.back": "Înapoi", + "subpages.index.add": "Adaugă o pagină nouă", + "subpages.index.add.first.text": "Această pagină nu conţine încă subpagini.", + "subpages.index.add.first.button": "Adaugă o primă subpagină", + "subpages.index.visible": "Pagini vizibile", + "subpages.index.visible.help": "Trage aici pagini pentru a le face vizibile.", + "subpages.index.invisible": "Pagini invizibile", + "subpages.index.invisible.help": "Trage aici pagini pentru a le face invizibile.", + "subpages.add.error": "This page is not allowed to have subpages", + "subpages.add.error.more": "This page cannot have any more subpages", + "subpages.error.missing": "Pagina nu a putut fi găsită!", + "files": "Fişiere", + "files.index.headline": "Fişiere", + "files.index.back": "Înapoi", + "files.index.upload": "Încarcă un nou fişier", + "files.index.upload.first.text": "Această pagină nu conţine încă niciun fişier.", + "files.index.upload.first.button": "Încarcă un prim fişier", + "files.index.edit": "Editează", + "files.index.delete": "Şterge", + "files.index.error.disabled": "The page is not allowed to have any files", + "files.add.error.max": "The maximum number of files for the current page has been reached.", + "files.add.error.extension.missing": "You cannot upload files without extension", + "files.add.error.extension.forbidden": "Forbidden file extension", + "files.add.error.mime.forbidden": "Forbidden mime type", + "files.add.error.htaccess": "htaccess files cannot be uploaded", + "files.add.error.invisible": "Invisible files cannot be uploaded", + "files.add.blueprint.type.error": "Page only allows:", + "files.add.blueprint.size.error": "Page only allows file size of", + "files.show.name.label": "Nume fişier", + "files.show.info.label": "Tip / Mărime / Dimensiuni", + "files.show.link.label": "Calea către imagine", + "files.show.open": "Afişează / descarcă fişierul", + "files.show.back": "Înapoi", + "files.show.replace": "Înlocuieşte", + "files.show.delete": "Şterge", + "files.show.error.rename": "Fişierul nu a putut fi redenumit!", + "files.show.error.form": "Completează toate câmpurile corect, te rog!", + "files.upload.drop": "Trage fişiere aici…", + "files.upload.click": "…sau click pentru a le încărca", + "files.replace.drop": "Trage un fişier aici…", + "files.replace.click": "…sau click pentru a-l înlocui", + "files.replace.error.type": "Fişierul încărcat trebuie să fie de acelaşi tip!", + "files.delete.headline": "Sigur vrei să ştergi acest fişier?", + "files.error.missing.page": "Pagina nu a putut fi găsită!", + "files.error.missing.file": "Fişierul nu a putut fi găsit!", + "users": "Utilizatori", + "users.index.headline": "Toţi utilizatorii", + "users.index.add": "Adaugă un utilizator nou", + "users.index.edit": "Editează", + "users.index.delete": "Şterge", + "users.form.username.label": "Nume utilizator", + "users.form.username.placeholder": "Numele tău de utilizator", + "users.form.username.help": "Caractere permise: litere mici de la a la z, cifre şi cratime.", + "users.form.username.readonly": "Numele de utilizator nu poate fi schimbat!", + "users.form.firstname.label": "Prenume", + "users.form.lastname.label": "Nume", + "users.form.email.label": "Email", + "users.form.email.placeholder": "email@exemplu.com", + "users.form.password.label": "Parola", + "users.form.password.confirm.label": "Confirmare parolă", + "users.form.password.new.label": "Parolă nouă", + "users.form.password.new.confirm.label": "Confirmarea parolei noi", + "users.form.password.new.help": "Lasă necompletat pentru a păstra parola curentă.", + "users.form.language.label": "Limba", + "users.form.role.label": "Rol", + "users.form.options.headline": "Opţiuni cont", + "users.form.options.message": "Trimite email", + "users.form.options.delete": "Şterge contul", + "users.form.avatar.headline": "Poza de profil", + "users.form.avatar.upload": "Încarcă o poză de profil", + "users.form.avatar.replace": "Înlocuieşte poza de profil", + "users.form.avatar.delete": "Şterge poza de profil", + "users.form.back": "Înapoi la utilizatori", + "users.form.error.password.confirm": "Confirmă parola, te rog!", + "users.form.error.update": "Contul de utilizator nu a putut fi actualizat!", + "users.form.error.update.rights": "You are not allowed to update this user", + "users.form.error.create": "Contul de utilizator nu a putut fi creat!", + "users.form.error.permissions.title": "Directorul /site/accounts nu are permisiuni de scriere.", + "users.form.error.permissions.text": "Verifică dacă directorul /site/accounts există şi are permisiuni de scriere.", + "users.delete.headline": "Sigur vrei să ştergi acest cont de utilizator?", + "users.delete.error": "Contul de utilizator nu a putut fi şters!", + "users.delete.error.permission": "You are not allowed to delete users", + "users.delete.error.permission.single": "You are not allowed to delete this user", + "users.delete.error.lastadmin": "You cannot delete the last admin", + "users.avatar.drop": "Trage o poză de profil aici…", + "users.avatar.click": "…sau click pentru încărcare", + "users.avatar.error.type": "Poţi încărca doar fişiere JPG, PNG sau GIF.", + "users.avatar.error.folder.headline": "Directorul /assets/avatars nu are permisiuni de scriere.", + "users.avatar.error.folder.text": "Verifică dacă directorul /assets/avatars există şi are permisiuni de scriere.", + "users.avatar.error.permission": "You are not allowed to change the avatar", + "users.avatar.delete.error": "Poza de profil nu a putut fi ştearsă!", + "users.avatar.delete.error.permission": "You are not allowed to delete the avatar of this user", + "users.avatar.delete.success": "Poza de profil a fost ştearsă!", + "users.avatar.missing": "This user has no avatar", + "users.error.missing": "Contul de utilizator nu a putut fi găsit!", + "user.error.lastadmin": "You are the only admin. This cannot be changed.", + "form.error.missing": "The form cannot be found", + "form.construct.error.invalid": "Invalid form construction method", + "fields.required": "Obligatoriu", + "fields.date.label": "Data", + "fields.date.months": [ + "Ianuarie", + "Februarie", + "Martie", + "Aprilie", + "Mai", + "Iunie", + "Iulie", + "August", + "Septembrie", + "Octombrie", + "Noiembrie", + "Decembrie" + ], + "fields.date.weekdays": [ + "Duminica", + "Luni", + "Marţi", + "Miercuri", + "Joi", + "Vineri", + "Sâmbătă" + ], + "fields.date.weekdays.short": [ + "Dum", + "Lun", + "Mar", + "Mie", + "Joi", + "Vin", + "Sâm" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "email@exemplu.com", + "fields.number.label": "Număr", + "fields.number.placeholder": "#", + "fields.page.label": "Pagină", + "fields.page.placeholder": "calea/către/pagină", + "fields.password.label": "Parola", + "fields.structure.add": "Adaugă", + "fields.structure.add.first": "Completează formularul!", + "fields.structure.empty": "Nicio inserţie.", + "fields.structure.entry.error": "The item could not be found", + "fields.structure.cancel": "Renunţă", + "fields.structure.save": "Salvează", + "fields.structure.edit": "Editează", + "fields.structure.delete": "Şterge", + "fields.structure.delete.label": "Do you really want to delete this entry?", + "fields.tags.label": "Etichete", + "fields.tel.label": "Telefon", + "fields.textarea.buttons.bold.label": "Text îngroşat", + "fields.textarea.buttons.bold.text": "Text îngroşat", + "fields.textarea.buttons.italic.label": "Text înclinat", + "fields.textarea.buttons.italic.text": "Text înclinat", + "fields.textarea.buttons.link.label": "Link", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Imagine", + "fields.textarea.buttons.file.label": "Fişier", + "fields.toggle.yes": "Da", + "fields.toggle.no": "Nu", + "fields.toggle.on": "On", + "fields.toggle.off": "Off", + "fields.error.missing.controller": "The field controller file is missing", + "fields.error.missing.class": "The field controller class is missing", + "fields.error.route.invalid": "Invalid field route", + "fields.error.extended": "The field cannot be extended", + "editor.link.url.label": "Inserează URL", + "editor.link.text.label": "Link text", + "editor.link.text.help": "Linkul text este opţional.", + "editor.email.address.label": "Inserează adresa email", + "editor.email.address.placeholder": "email@exemplu.com", + "editor.email.text.label": "Link text", + "editor.email.text.help": "Linkul text este opţional.", + "editor.file.empty": "Această pagină nu conţine fişiere.", + "editor.image.empty": "Această pagină nu conţine imagini.", + "autocomplete.method.error": "Invalid autocomplete method", + "blueprints.error.default.missing": "Missing default blueprint", + "error": "Eroare", + "error.headline": "Eroare" +} \ No newline at end of file diff --git a/panel/app/translations/ro/package.json b/panel/app/translations/ro/package.json new file mode 100644 index 0000000..a0e3e8a --- /dev/null +++ b/panel/app/translations/ro/package.json @@ -0,0 +1,4 @@ +{ + "title": "Română", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/ru/core.json b/panel/app/translations/ru/core.json new file mode 100644 index 0000000..c58a509 --- /dev/null +++ b/panel/app/translations/ru/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Отменить", + "add": "Добавить", + "addit": "Добавить и Изменить", + "save": "Сохранить", + "saved": "Сохранено", + "change": "Изменить", + "delete": "Удалить", + "insert": "Вставить", + "ok": "Ок", + "routes.error.invalid": "Неверный URL Панели", + "controller.error.invalid": "Неверный контроллер", + "controller.error.action": "Неверное действие", + "view.error.invalid": "Неверный вид:", + "options.show": "Показать опции", + "options.hide": "Скрыть опции", + "installation": "Установка", + "installation.check.headline": "Установка панели Kirby", + "installation.check.text": "Во время установки Kirby возникли следующие проблемы…", + "installation.check.retry": "Повторить", + "installation.check.error": "Не все прошло так гладко :(", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts не доступно для записи", + "installation.check.error.avatars": "/assets/avatars не доступно для записи", + "installation.check.error.blueprints": "Пожалуйста, создайте папку /site/blueprints", + "installation.check.error.content": "Папка content в все вложенные папки и файлы должны быть доступны для записи.", + "installation.check.error.thumbs": "Папка thumbs должна быть доступна для записи.", + "installation.signup.username.label": "Создайте первый аккаунт пользователя", + "installation.signup.username.placeholder": "Логин", + "installation.signup.email.label": "Эл.почта", + "installation.signup.email.placeholder": "pochta@domen.com", + "installation.signup.password.label": "Пароль", + "installation.signup.language.label": "Язык", + "installation.signup.button": "Создать аккаунт", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Логин", + "login.password.label": "Пароль", + "login.error": "Неверный логин или пароль", + "login.button": "Log in", + "login.log.error.permissions": "Файл журнала входов не доступен для записи.", + "logout": "Log out", + "topbar.error.class.definition": "Missing topbar definition for class:", + "dashboard": "Панель управления", + "dashboard.index.pages.title": "Страницы", + "dashboard.index.pages.edit": "Настроить", + "dashboard.index.pages.add": "Добавить", + "dashboard.index.site.title": "Адрес вашего сайта", + "dashboard.index.account.title": "Ваш аккаунт", + "dashboard.index.account.edit": "Настроить", + "dashboard.index.metatags.title": "Переменные сайта", + "dashboard.index.metatags.edit": "Настроить", + "dashboard.index.history.title": "Ваши последние правки", + "dashboard.index.history.text": "Последние измененные вами страницы будут показаны здесь для быстрого доступа к ним.", + "dashboard.index.license.title": "Лицензия Kirby", + "dashboard.index.license.text": "Кажется, вы запустили Kirby на общедоступном сервере без действующей лицензии!\n\nПожалуйста, поддержите Kirby и (link: {buy} text: купите лицензию)\n\nЕсли у вас уже есть лицензионный ключ, просто добавьте его в ваш конфигурационный файл: (link: {docs} text: site/config/config.php)", + "metatags": "Переменные сайта", + "metatags.info": "Информация о Kirby", + "metatags.license": "Лицензия Kirby", + "metatags.version.toolkit": "Версия Инструментария", + "metatags.version.kirby": "Версия Kirby", + "metatags.version.panel": "Версия панели", + "metatags.back": "Назад в панель управления", + "metatags.files": "Файлы сайта", + "site.delete.error": "Сайт не может быть удален", + "pages.show.settings": "Настройки страницы", + "pages.show.preview": "Предпросмотр", + "pages.show.template": "Шаблон", + "pages.show.changeurl": "Изменить ссылку (ЧПУ)", + "pages.show.invisible": "Статус: не отображается", + "pages.show.visible": "Статус: отображается", + "pages.show.changes.text": "Вы не сохранили изменения!", + "pages.show.changes.button": "Сброс", + "pages.show.delete": "Удалить эту страницу", + "pages.show.subpages.title": "Страницы", + "pages.show.subpages.edit": "Настроить", + "pages.show.subpages.add": "Добавить", + "pages.show.subpages.empty": "Для этой страницы нет подстраниц", + "pages.show.files.title": "Файлы", + "pages.show.files.edit": "Настроить", + "pages.show.files.add": "Добавить", + "pages.show.files.empty": "Для этой страницы нет файлов", + "pages.show.error.permissions.title": "Эта страница не доступна для записи", + "pages.show.error.permissions.text": "Пожалуйста, проверьте права для папки content и всех файлов.", + "pages.show.error.permissions.retry": "Повторить", + "pages.show.error.notitle.title": "В этом шаблоне формы должно быть поле для названия", + "pages.show.error.notitle.text": "Пожалуйста, заполните название и повторите снова", + "pages.show.error.notitle.retry": "Повторить", + "pages.show.error.form": "Пожалуйста, заполните все необходимые поля корректно", + "pages.add.title.label": "Создать новую страницу", + "pages.add.title.placeholder": "Название", + "pages.add.url.label": "понятная ссылка (ЧПУ)", + "pages.add.url.enter": "(введите название страницы)", + "pages.add.url.close": "Закрыть", + "pages.add.url.help": "Формат: нижние латинские буквы, цифры и дефисы", + "pages.add.template.label": "Шаблон", + "pages.add.error.create": "Страница не может быть создана", + "pages.add.error.title": "Отсутствует название", + "pages.add.error.template": "Отсутствует шаблон", + "pages.add.error.max.headline": "Предельное количество страниц", + "pages.add.error.max.text": "Для данной страницы достигнут максимальный предел подстраниц.", + "pages.url.uid.label": "ссылка (ЧПУ)", + "pages.url.uid.label.option": "сформировать", + "pages.url.error.exists": "Страница с такой же понятной ссылкой (ЧПУ) уже существует", + "pages.url.error.move": "Понятная ссылка (ЧПУ) не может быть изменена", + "pages.url.error.rights": "Вы не можете изменить URL этой страницы", + "pages.template.select.label": "Шаблон", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Позиция", + "pages.toggle.invisible": "не отображается", + "pages.toggle.publish": "Вы действительно хотите изменить статус этой страницы на **отображается?**", + "pages.toggle.hide": "Вы действительно хотите изменить статус этой страницы на **не отображается?**", + "pages.toggle.error.error": "Статус страницы ошибки не может быть изменен", + "pages.delete.headline": "Вы действительно хотите удалить эту страницу?", + "pages.delete.error.home.headline": "Индексная (домашняя) страница не может быть удалена", + "pages.delete.error.home.text": "Вы пытаетесь удалить индексную (домашнюю) страницу. Это невозможно, так как может привести к непредсказуемым результатам.", + "pages.delete.error.error.headline": "Страница ошибок (404) не может быть удалена", + "pages.delete.error.error.text": "Вы пытаетесь удалить страницу ошибок (Error 404 Page). Это невозможно, так как может привести к непредсказуемым результатам.", + "pages.delete.error.children.headline": "Страница не может быть удалена", + "pages.delete.error.children.text": "Для этой страницы существуют подстраницы. Пожалуйста, удалите сначала подстраницы.", + "pages.delete.error.blocked.headline": "Эта страница не может быть удалена", + "pages.delete.error.blocked.text": "Эта страница заблокирована и не может быть удалена в настоящий момент.", + "pages.search.help": "Поиск страниц по ссылкам. Для перемещения по результатам поиска используйте стрелки на клавиатуре ВВЕРХ и ВНИЗ. Для открытия страницы, нажмите ВВОД.", + "pages.search.noresults": "Нет результатов по вашему запросу. Пожалуйста, проверьте строку поиска.", + "pages.error.missing": "Страница не найдена", + "subpages": "Страницы", + "subpages.index.headline": "Страниц для", + "subpages.index.back": "Назад", + "subpages.index.add": "Добавить новую страницу", + "subpages.index.add.first.text": "Для этой страницы пока нет подстраниц", + "subpages.index.add.first.button": "Добавить первую страницу", + "subpages.index.visible": "Видимые страницы", + "subpages.index.visible.help": "Перетащите невидимые страницы сюда для их публикации (сортировки в меню).", + "subpages.index.invisible": "Невидимые страницы", + "subpages.index.invisible.help": "Перетащите видимые страницы сюда для их сокрытия (удаления из меню).", + "subpages.add.error": "Эта страница не имеет подстраниц", + "subpages.add.error.more": "Эта страница не может иметь больше подстраниц", + "subpages.error.missing": "Страница не найдена", + "files": "Файлы", + "files.index.headline": "Файлов для", + "files.index.back": "назад", + "files.index.upload": "Закачать новый файл", + "files.index.upload.first.text": "Для этой страницы пока нет файлов", + "files.index.upload.first.button": "Закачать первый файл", + "files.index.edit": "Настроить", + "files.index.delete": "Удалить", + "files.index.error.disabled": "The page is not allowed to have any files", + "files.add.error.max": "The maximum number of files for the current page has been reached.", + "files.add.error.extension.missing": "You cannot upload files without extension", + "files.add.error.extension.forbidden": "Forbidden file extension", + "files.add.error.mime.forbidden": "Forbidden mime type", + "files.add.error.htaccess": "htaccess files cannot be uploaded", + "files.add.error.invisible": "Invisible files cannot be uploaded", + "files.add.blueprint.type.error": "Page only allows:", + "files.add.blueprint.size.error": "Page only allows file size of", + "files.show.name.label": "Имя файла", + "files.show.info.label": "Тип / размер / Разрешение", + "files.show.link.label": "Публичная ссылка", + "files.show.open": "Показать/скачать файл", + "files.show.back": "Назад", + "files.show.replace": "Заменить", + "files.show.delete": "Удалить", + "files.show.error.rename": "Файл не может быть переименован", + "files.show.error.form": "Пожалуйста, заполните все необходимые поля корректно", + "files.upload.drop": "Перетащите файлы сюда…", + "files.upload.click": "…или кликните для выбора", + "files.replace.drop": "Перетащите файлы сюда…", + "files.replace.click": "…или кликните для выбора", + "files.replace.error.type": "Закачиваемый файл должен иметь такое же расширение (тип)", + "files.delete.headline": "Вы действительно хотите удалить файл?", + "files.error.missing.page": "Страница не найдена", + "files.error.missing.file": "Файл не найден", + "users": "Пользователи", + "users.index.headline": "Все пользователи", + "users.index.add": "Добавить нового пользователя", + "users.index.edit": "Настроить", + "users.index.delete": "Удалить", + "users.form.username.label": "Логин", + "users.form.username.placeholder": "Ваш логин", + "users.form.username.help": "Формат: нижние латинские буквы, цифры и дефисы", + "users.form.username.readonly": "Логин не может быть изменен", + "users.form.firstname.label": "Имя", + "users.form.lastname.label": "Фамилия", + "users.form.email.label": "Эл.почта", + "users.form.email.placeholder": "pochta@domen.com", + "users.form.password.label": "Пароль", + "users.form.password.confirm.label": "Подтвердите пароль", + "users.form.password.new.label": "Новый пароль", + "users.form.password.new.confirm.label": "Подтвердите новый пароль", + "users.form.password.new.help": "Оставьте пустым, чтобы не менять пароль", + "users.form.language.label": "Язык", + "users.form.role.label": "Роль", + "users.form.options.headline": "Опции аккаунта", + "users.form.options.message": "Отправить эл.почту", + "users.form.options.delete": "Удалить аккаунт", + "users.form.avatar.headline": "Аватар (фото)", + "users.form.avatar.upload": "Закачать картинку для аккаунта", + "users.form.avatar.replace": "Заменить картинку для аккаунта", + "users.form.avatar.delete": "Удалить картинку для аккаунта", + "users.form.back": "назад к аккаунтам", + "users.form.error.password.confirm": "Пожалуйста, подтвердите пароль", + "users.form.error.update": "Аккаунт не может быть изменен", + "users.form.error.update.rights": "You are not allowed to update this user", + "users.form.error.create": "Аккаунт не может быть создан", + "users.form.error.permissions.title": "Папка account не доступна для записи", + "users.form.error.permissions.text": "Пожалуйста, убедитесь, что папка /site/accounts существует и доступна для записи.", + "users.delete.headline": "Вы действительно хотите удалить аккаунт?", + "users.delete.error": "Аккаунт не может быть удален", + "users.delete.error.permission": "You are not allowed to delete users", + "users.delete.error.permission.single": "You are not allowed to delete this user", + "users.delete.error.lastadmin": "Вы не можете удалить единственного администратора", + "users.avatar.drop": "Перетащите картинку для аккаунта сюда…", + "users.avatar.click": "…или кликните для выбора", + "users.avatar.error.type": "Формат файлов картинок может быть JPG, PNG или GIF", + "users.avatar.error.folder.headline": "Папка avatar не доступна для записи", + "users.avatar.error.folder.text": "Пожалуйста, убедитесь, что папка /assets/avatars существует и доступна для записи перед добавлением аватаров (фото) к аккаунтам.", + "users.avatar.error.permission": "You are not allowed to change the avatar", + "users.avatar.delete.error": "Аватар (фото) к аккаунту не может быть удален", + "users.avatar.delete.error.permission": "You are not allowed to delete the avatar of this user", + "users.avatar.delete.success": "Аватар (фото) к аккаунту удален", + "users.avatar.missing": "У пользователя нет аватара", + "users.error.missing": "Аккаунт не найден", + "user.error.lastadmin": "You are the only admin. This cannot be changed.", + "form.error.missing": "The form cannot be found", + "form.construct.error.invalid": "Invalid form construction method", + "fields.required": "Необходимо", + "fields.date.label": "Дата", + "fields.date.months": [ + "Январь", + "Февраль", + "Март", + "Апрель", + "Май", + "Июнь", + "Июль", + "Август", + "Сентябрь", + "Октябрь", + "Ноябрь", + "Декабрь" + ], + "fields.date.weekdays": [ + "Воскресенье", + "Понедельник", + "Вторник", + "Среда", + "Четверг", + "Пятница", + "Суббота" + ], + "fields.date.weekdays.short": [ + "Вс", + "Пн", + "Вт", + "Ср", + "Чт", + "Пт", + "Сб" + ], + "fields.email.label": "Эл.почта", + "fields.email.placeholder": "pochta@domen.com", + "fields.number.label": "Номер", + "fields.number.placeholder": "#", + "fields.page.label": "Страница", + "fields.page.placeholder": "путь/к/странице", + "fields.password.label": "Пароль", + "fields.structure.add": "Добавить", + "fields.structure.add.first": "Добавить первую запись", + "fields.structure.empty": "Пока нет записей.", + "fields.structure.entry.error": "The item could not be found", + "fields.structure.cancel": "Отмена", + "fields.structure.save": "Сохранить", + "fields.structure.edit": "Настроить", + "fields.structure.delete": "Удалить", + "fields.structure.delete.label": "Do you really want to delete this entry?", + "fields.tags.label": "Тэги", + "fields.tel.label": "Телефон", + "fields.textarea.buttons.bold.label": "Жирный шрифт", + "fields.textarea.buttons.bold.text": "Жирный шрифт", + "fields.textarea.buttons.italic.label": "Наклонный шрифт", + "fields.textarea.buttons.italic.text": "Наклонный шрифт", + "fields.textarea.buttons.link.label": "Ссылка", + "fields.textarea.buttons.email.label": "Эл.почта", + "fields.textarea.buttons.image.label": "Картинка", + "fields.textarea.buttons.file.label": "Файл", + "fields.toggle.yes": "Да", + "fields.toggle.no": "Нет", + "fields.toggle.on": "Вкл", + "fields.toggle.off": "Выкл", + "fields.error.missing.controller": "The field controller file is missing", + "fields.error.missing.class": "The field controller class is missing", + "fields.error.route.invalid": "Invalid field route", + "fields.error.extended": "The field cannot be extended", + "editor.link.url.label": "Вставить ссылку", + "editor.link.text.label": "Текст ссылки", + "editor.link.text.help": "Текст ссылки не обязателен", + "editor.email.address.label": "Введите адрес эл.почты", + "editor.email.address.placeholder": "pochta@domen.com", + "editor.email.text.label": "Текст ссылки эл.почты", + "editor.email.text.help": "Текст ссылки эл.почты не обязателен", + "editor.file.empty": "Для этой страницы нет файлов", + "editor.image.empty": "Для этой страницы нет картинок", + "autocomplete.method.error": "Invalid autocomplete method", + "blueprints.error.default.missing": "Missing default blueprint", + "error": "Ошибка", + "error.headline": "Ошибка" +} \ No newline at end of file diff --git a/panel/app/translations/ru/package.json b/panel/app/translations/ru/package.json new file mode 100644 index 0000000..6d6c6ec --- /dev/null +++ b/panel/app/translations/ru/package.json @@ -0,0 +1,4 @@ +{ + "title": "Русский (Russian)‎", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/sv_SE/core.json b/panel/app/translations/sv_SE/core.json new file mode 100644 index 0000000..e5cafc8 --- /dev/null +++ b/panel/app/translations/sv_SE/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Avbryt", + "add": "Lägg till", + "addit": "Lägg till & redigera", + "save": "Spara", + "saved": "Sparad!", + "change": "Spara ändring", + "delete": "Radera", + "insert": "Infoga", + "ok": "Ok", + "routes.error.invalid": "Ogiltig URL för panel", + "controller.error.invalid": "Ogiltig kontroll", + "controller.error.action": "Ogiltig åtgärd", + "view.error.invalid": "Ogiltig vy:", + "options.show": "Visa alternativ", + "options.hide": "Göm alternativ", + "installation": "Installation", + "installation.check.headline": "Kirby Panel-installation", + "installation.check.text": "Kirby påträffade följande fel under installationen…", + "installation.check.retry": "Försök igen", + "installation.check.error": "Det finns några problem!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "Mappen /site/accounts saknar skrivrättigheter", + "installation.check.error.avatars": "Mappen /assets/avatars saknar skrivrättigheter", + "installation.check.error.blueprints": "Vänligen lägg till en mapp för /site/blueprints", + "installation.check.error.content": "Innehållet i mappen, alla dess filer och undermappar, måste ha skrivrättigheter.", + "installation.check.error.thumbs": "Mappen /thumbs måste ha skrivrättigheter.", + "installation.signup.username.label": "Skapa ditt första konto", + "installation.signup.username.placeholder": "Användarnamn", + "installation.signup.email.label": "E-mail", + "installation.signup.email.placeholder": "namn@exempel.se", + "installation.signup.password.label": "Lösenord", + "installation.signup.language.label": "Språk", + "installation.signup.button": "Skapa ett konto", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Användarnamn", + "login.password.label": "Lösenord", + "login.error": "Fel användarnamn eller lösenord", + "login.button": "Log in", + "login.log.error.permissions": "Logg-filen för inloggningar är ej skrivbar.", + "logout": "Log out", + "topbar.error.class.definition": "Missing topbar definition for class:", + "dashboard": "Kontrollpanel", + "dashboard.index.pages.title": "Sidor", + "dashboard.index.pages.edit": "Ändra", + "dashboard.index.pages.add": "Lägg till", + "dashboard.index.site.title": "Webbplatsens webbadress", + "dashboard.index.account.title": "Ditt konto", + "dashboard.index.account.edit": "Ändra", + "dashboard.index.metatags.title": "Webbplatsens metadata", + "dashboard.index.metatags.edit": "Ändra", + "dashboard.index.history.title": "Senast ändrade sidor", + "dashboard.index.history.text": "Dina senast ändrade sidor kommer att listas här för att underlätta att hitta dem igen senare.", + "dashboard.index.license.title": "Kirbylicens", + "dashboard.index.license.text": "Det verkar som att du kör Kirby på en publik server utan giltig licens. \n\nVänligen, stöd Kirby genom att (link: {buy} text: köpa en licens nu).\n\nOm du redan har en licens, se till att lägga till den i din konfigurationsfil: (link: {docs} text: site/config/config.php)", + "metatags": "Webbplatsens metadata", + "metatags.info": "Information om Kirby", + "metatags.license": "Kirbylicens", + "metatags.version.toolkit": "Version av Toolkit", + "metatags.version.kirby": "Version av Kirby", + "metatags.version.panel": "Version av Panel", + "metatags.back": "Tillbaka till kontrollpanelen", + "metatags.files": "Webbplatsfiler", + "site.delete.error": "Sidan kan inte raderas", + "pages.show.settings": "Inställningar för sida", + "pages.show.preview": "Förhandsvisa sida", + "pages.show.template": "Mall", + "pages.show.changeurl": "Ändra webbadress", + "pages.show.invisible": "Dold i meny", + "pages.show.visible": "Synlig i meny", + "pages.show.changes.text": "Du har ändringar som inte sparats!", + "pages.show.changes.button": "Skrota", + "pages.show.delete": "Radera sida", + "pages.show.subpages.title": "Undersidor", + "pages.show.subpages.edit": "Ändra", + "pages.show.subpages.add": "Lägg till", + "pages.show.subpages.empty": "Den här sidan har inga undersidor.", + "pages.show.files.title": "Filer", + "pages.show.files.edit": "Ändra", + "pages.show.files.add": "Lägg till", + "pages.show.files.empty": "Den här sidan har inga filer.", + "pages.show.error.permissions.title": "Sidan är ej skrivbar", + "pages.show.error.permissions.text": "Vänligen kontrollera skriv- och läsrättigheter för mappen /content samt dess underliggande filer och mappar.", + "pages.show.error.permissions.retry": "Försök igen", + "pages.show.error.notitle.title": "Blueprinten har inget titel-fält", + "pages.show.error.notitle.text": "Vänligen lägg till ett titel-fält och försök igen", + "pages.show.error.notitle.retry": "Försök igen", + "pages.show.error.form": "vänligen fyll i alla fält korrekt", + "pages.add.title.label": "Lägg till en ny sida", + "pages.add.title.placeholder": "Titel", + "pages.add.url.label": "Tillägg i webbadress", + "pages.add.url.enter": "(skriv in din titel)", + "pages.add.url.close": "Stäng", + "pages.add.url.help": "Format: gemener a-z, 0-9 och vanliga streck", + "pages.add.template.label": "Mall", + "pages.add.error.create": "Sidan kan ej skapas", + "pages.add.error.title": "Titel saknas", + "pages.add.error.template": "Mall saknas", + "pages.add.error.max.headline": "Nya sidor är ej tillåtet", + "pages.add.error.max.text": "Det maximala antalet undersidor för den här sidan har nåtts.", + "pages.url.uid.label": "Tillägg i webbadress", + "pages.url.uid.label.option": "Skapa utifrån titel", + "pages.url.error.exists": "En sida med samma webbadress existerar redan", + "pages.url.error.move": "Tillägget i webbadress kan ej ändras", + "pages.url.error.rights": "Du kan inte ändra URL:en på denna sidan", + "pages.template.select.label": "Mall", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Position", + "pages.toggle.invisible": "dold", + "pages.toggle.publish": "Är du säker på att du vill ändra sidan status till **synlig i meny?**", + "pages.toggle.hide": "Är du säker på att du vill ändra sidan status till **dold i meny?**", + "pages.toggle.error.error": "Statusen för felsidan kan inte ändras", + "pages.delete.headline": "Vill du verkligen radera sidan?", + "pages.delete.error.home.headline": "Startsidan kan ej raderas", + "pages.delete.error.home.text": "Du försöker radera startsidan. Det är inte möjligt och får oönskad effekt.", + "pages.delete.error.error.headline": "Felsidan kan inte tas bort", + "pages.delete.error.error.text": "Du försöker ta bort felsidan. Det är inte möjligt och får oönskad effekt.", + "pages.delete.error.children.headline": "Den här sidan kan ej raderas", + "pages.delete.error.children.text": "Den här sidan har undersidor och kan ej raderas. Vänlig radera alla undersidor först.", + "pages.delete.error.blocked.headline": "Den här sidan kan ej raderas", + "pages.delete.error.blocked.text": "Den här sidan är låst och kan ej raderas.", + "pages.search.help": "Sök bland alla sidor via dess webbadress. Navigera upp och ner genom sökresultaten med piltangenterna och tryck enter-/retur-knapp för att redigera markerad sida.", + "pages.search.noresults": "Det finns inga resultat på din sökning. Var vänlig och försök igen med en annan webbadress.", + "pages.error.missing": "Sidan kan ej hittas", + "subpages": "Alla sidor", + "subpages.index.headline": "Alla undersidor i", + "subpages.index.back": "Tillbaka", + "subpages.index.add": "Lägg till en ny sida", + "subpages.index.add.first.text": "Den här sidan har inga undersidor än", + "subpages.index.add.first.button": "Lägg till den första sidan", + "subpages.index.visible": "Synliga sidor i meny", + "subpages.index.visible.help": "Dra dolda sidor hit för att sortera dem/göra dem synliga.", + "subpages.index.invisible": "Dolda sidor (visas ej i meny)", + "subpages.index.invisible.help": "Dra synliga sidor hit för att dölja dem.", + "subpages.add.error": "Den här sidan kan inte ha undersidor", + "subpages.add.error.more": "Den här sidan kan inte ha fler undersidor", + "subpages.error.missing": "Sidan kan ej hittas", + "files": "Filer", + "files.index.headline": "Filer för", + "files.index.back": "Tillbaka", + "files.index.upload": "Ladda upp en ny fil", + "files.index.upload.first.text": "Den här sidan har inga filer än", + "files.index.upload.first.button": "Ladda upp första filen", + "files.index.edit": "Ändra", + "files.index.delete": "Radera", + "files.index.error.disabled": "Den här sidan kan inte ha några filer", + "files.add.error.max": "Det maximala antalet filer för den här sidan har nåtts", + "files.add.error.extension.missing": "Du kan inte ladda upp filer utan filändelse", + "files.add.error.extension.forbidden": "Filändelsen är ej tillåten", + "files.add.error.mime.forbidden": "Ogiltig MIME-typ", + "files.add.error.htaccess": "Htaccess-filer är ej tillåtet att ladda upp.", + "files.add.error.invisible": "Osynliga filer är ej tillåtet att ladda upp.", + "files.add.blueprint.type.error": "Sidan tillåter endast:", + "files.add.blueprint.size.error": "Sidan tillåter endast en filstorlek på", + "files.show.name.label": "Filnamn", + "files.show.info.label": "Filtyp / storlek / dimensioner", + "files.show.link.label": "Publik webbadress", + "files.show.open": "Visa/ladda ner fil", + "files.show.back": "Tillbaka", + "files.show.replace": "Ersätt fil", + "files.show.delete": "Radera", + "files.show.error.rename": "Filen kan ej döpas om", + "files.show.error.form": "Var vänlig och fyll i alla fält korrekt", + "files.upload.drop": "Släpp filer här…", + "files.upload.click": "…eller klicka för att ladda upp", + "files.replace.drop": "Släpp en fil här …", + "files.replace.click": "… eller klicka för att välja en ny fil", + "files.replace.error.type": "Den uppladdade filen måste vara av samma filtyp", + "files.delete.headline": "Vill du verkligen ta bort denna fil?", + "files.error.missing.page": "Sidan kan ej hittas", + "files.error.missing.file": "Filen kan ej hittas", + "users": "Användare", + "users.index.headline": "Alla användare", + "users.index.add": "Lägg till en ny användare", + "users.index.edit": "Ändra", + "users.index.delete": "Radera", + "users.form.username.label": "Användarnamn", + "users.form.username.placeholder": "Ditt användarnamn", + "users.form.username.help": "Tillåtna tecken: gemener a-z, 0-9 och streck", + "users.form.username.readonly": "Användarnamnet kan ej ändras", + "users.form.firstname.label": "Förnamn", + "users.form.lastname.label": "Efternamn", + "users.form.email.label": "E-mail", + "users.form.email.placeholder": "namn@exampel.se", + "users.form.password.label": "Lösenord", + "users.form.password.confirm.label": "Bekräfta lösenord", + "users.form.password.new.label": "Nytt lösenord", + "users.form.password.new.confirm.label": "Bekräfta nytt lösenord", + "users.form.password.new.help": "Lämna blankt för att behålla nuvarande lösenord", + "users.form.language.label": "Språk", + "users.form.role.label": "Roll", + "users.form.options.headline": "Kontoinställningar", + "users.form.options.message": "Skicka mail", + "users.form.options.delete": "Radera konto", + "users.form.avatar.headline": "Profilbild", + "users.form.avatar.upload": "Ladda upp profilbild", + "users.form.avatar.replace": "Byt ut profilbild", + "users.form.avatar.delete": "Radera profilbild", + "users.form.back": "Tillbaka till användare", + "users.form.error.password.confirm": "Vänlig bekräfta ditt lösenord", + "users.form.error.update": "Användaren kan ej uppdateras", + "users.form.error.update.rights": "Du får inte uppdatera den här användaren", + "users.form.error.create": "Användaren kan ej skapas", + "users.form.error.permissions.title": "Mappen är ej skrivbar", + "users.form.error.permissions.text": "Var vänlig och kontroller att mappen sites/accounts existerar och har skrivrättigheter.", + "users.delete.headline": "Vill du verkligen radera användaren?", + "users.delete.error": "Användaren kan ej raderas", + "users.delete.error.permission": "Du får inte radera användare", + "users.delete.error.permission.single": "Du får inte radera den här användaren", + "users.delete.error.lastadmin": "Du kan inte radera sista administratören", + "users.avatar.drop": "Släpp en profilbild här…", + "users.avatar.click": "…eller klicka för att ladda upp", + "users.avatar.error.type": "Du kan bara ladda upp bilder i följande filformat: JPG, PNG och GIF", + "users.avatar.error.folder.headline": "avatar-mappen är inte skrivbar", + "users.avatar.error.folder.text": "Var vänlig och kontrollera att mappen /assets/avatars existerar och har skrivrättigheter för att kunna ladda upp profilbilder.", + "users.avatar.error.permission": "Du får inte ändra avataren", + "users.avatar.delete.error": "Profilbilden kan ej raderas", + "users.avatar.delete.error.permission": "Du får inte radera avataren för den här användaren", + "users.avatar.delete.success": "Profilbilden har raderas", + "users.avatar.missing": "Den här användare saknar avatar", + "users.error.missing": "Användaren kan ej hittas", + "user.error.lastadmin": "Du är ensam administratör. Det går inte att ändra.", + "form.error.missing": "Formuläret kan ej hittas", + "form.construct.error.invalid": "Ogiltig metod för konstruktion av formulär", + "fields.required": "Ifyllt fält krävs.", + "fields.date.label": "Datum", + "fields.date.months": [ + "Januari", + "Februari", + "Mars", + "April", + "Maj", + "Juni", + "Juli", + "Augusti", + "September", + "Oktober", + "November", + "December" + ], + "fields.date.weekdays": [ + "Söndag", + "Måndag", + "Tisdag", + "Onsdag", + "Torsdag", + "Fredag", + "Lördag" + ], + "fields.date.weekdays.short": [ + "Sön", + "Mån", + "Tis", + "Ons", + "Tor", + "Fre", + "Lör" + ], + "fields.email.label": "E-mail", + "fields.email.placeholder": "namn@exempel.se", + "fields.number.label": "Nummer", + "fields.number.placeholder": "#", + "fields.page.label": "Sida", + "fields.page.placeholder": "sökväg/till/sida", + "fields.password.label": "Lösenord", + "fields.structure.add": "Lägg till", + "fields.structure.add.first": "Lägg till första posten", + "fields.structure.empty": "Inga poster än.", + "fields.structure.entry.error": "Artikeln kan ej hittas", + "fields.structure.cancel": "Avbryt", + "fields.structure.save": "Spara", + "fields.structure.edit": "Ändra", + "fields.structure.delete": "Radera", + "fields.structure.delete.label": "Vill du verkligen radera den här posten?", + "fields.tags.label": "Taggar", + "fields.tel.label": "Telefon", + "fields.textarea.buttons.bold.label": "Fet text", + "fields.textarea.buttons.bold.text": "Fet text", + "fields.textarea.buttons.italic.label": "Kursiv text", + "fields.textarea.buttons.italic.text": "Kursiv text", + "fields.textarea.buttons.link.label": "Länk", + "fields.textarea.buttons.email.label": "E-mail", + "fields.textarea.buttons.image.label": "Bilder", + "fields.textarea.buttons.file.label": "Fil", + "fields.toggle.yes": "Ja", + "fields.toggle.no": "Nej", + "fields.toggle.on": "På", + "fields.toggle.off": "Av", + "fields.error.missing.controller": "Filen för fältkontroll saknas", + "fields.error.missing.class": "Klassen för fältkontroll saknas", + "fields.error.route.invalid": "Invalid field route", + "fields.error.extended": "Fältet kan inte förlängas", + "editor.link.url.label": "Lägg till webbadress", + "editor.link.text.label": "Länktext", + "editor.link.text.help": "Länktext är frivillig", + "editor.email.address.label": "Lägg till en emailaddress", + "editor.email.address.placeholder": "mail@exempel.se", + "editor.email.text.label": "Länktext", + "editor.email.text.help": "Länktext är frivillig", + "editor.file.empty": "Den här sidan saknar filer", + "editor.image.empty": "Den här sidan saknar bilder", + "autocomplete.method.error": "Ogiltig metod för automatisk komplettering", + "blueprints.error.default.missing": "Standard-blueprint saknas", + "error": "Fel", + "error.headline": "Error" +} \ No newline at end of file diff --git a/panel/app/translations/sv_SE/package.json b/panel/app/translations/sv_SE/package.json new file mode 100644 index 0000000..6b56c58 --- /dev/null +++ b/panel/app/translations/sv_SE/package.json @@ -0,0 +1,4 @@ +{ + "title": "Svenska", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/tr/core.json b/panel/app/translations/tr/core.json new file mode 100644 index 0000000..c57e840 --- /dev/null +++ b/panel/app/translations/tr/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "İptal", + "add": "Ekle", + "addit": "Ekle & Düzenle", + "save": "Kaydet", + "saved": "Kaydedildi!", + "change": "Değiştir", + "delete": "Sil", + "insert": "Ekle", + "ok": "Tamam", + "routes.error.invalid": "Geçersiz Panel Adresi", + "controller.error.invalid": "Geçersiz denetleyici", + "controller.error.action": "Geçersiz eylem", + "view.error.invalid": "Geçersiz görünüm:", + "options.show": "Seçenekleri göster", + "options.hide": "Seçenekleri gizle", + "installation": "Kurulum", + "installation.check.headline": "Kirby Panel Kurulumu", + "installation.check.text": "Kirby kurulum aşamasında belirtilen sorunla karşılaştı…", + "installation.check.retry": "Tekrar Dene", + "installation.check.error": "Bazı sorunlar mevcut!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts klasörü yazılabilir değil", + "installation.check.error.avatars": "/assets/avatars klasörü yazılabilir değil", + "installation.check.error.blueprints": "Lütfen şu klasörü oluşturun : /site/blueprints", + "installation.check.error.content": "Content adındaki klasör ve içindeki tüm klasörler, dosyalar yazılabilir olmalı.", + "installation.check.error.thumbs": "thumb klasörü yazılabilir olmalı", + "installation.signup.username.label": "İlk hesabını oluştur", + "installation.signup.username.placeholder": "Kullanıcı Adı", + "installation.signup.email.label": "E-Posta", + "installation.signup.email.placeholder": "eposta@ornek.com", + "installation.signup.password.label": "Şifre", + "installation.signup.language.label": "Dil", + "installation.signup.button": "Hesabını oluştur", + "login": "Giriş", + "login.welcome": "Lütfen yeni hesabınız ile giriş yapın", + "login.username.label": "Kullanıcı Adı", + "login.password.label": "Şifre", + "login.error": "Geçersiz kullanıcı adı veya şifre", + "login.button": "Giriş", + "login.log.error.permissions": "Giriş günlük kayıt dosyası yazılabilir değil", + "logout": "Güvenli çıkış", + "topbar.error.class.definition": "Sınıfın üstbar tanımlaması eksik:", + "dashboard": "Kontrol Paneli", + "dashboard.index.pages.title": "Sayfalar", + "dashboard.index.pages.edit": "Düzenle", + "dashboard.index.pages.add": "Ekle", + "dashboard.index.site.title": "Website Adresi", + "dashboard.index.account.title": "Hesap Bilgilerin", + "dashboard.index.account.edit": "Düzenle", + "dashboard.index.metatags.title": "Site Bilgileri", + "dashboard.index.metatags.edit": "Düzenle", + "dashboard.index.history.title": "Son güncellemelerin", + "dashboard.index.history.text": "Son güncellemelerin, daha sonra tekrar kolayca bulabilmen için burada görünecek", + "dashboard.index.license.title": "Kirby lisans", + "dashboard.index.license.text": "Kirby'yi canlı sunucuda geçerli bir lisansınız olmadan kullanıyor görünüyorsunuz!\n\nLütfen, Kirby'yi destekleyin ve (link: {buy} text: bir lisans satın alın)\n\nEğer bir lisans anahtarınız var ise, bunu yapılandırma dosyanıza ekleyin: (link: {docs} text: site/config/config.php)", + "metatags": "Site seçenekleri", + "metatags.info": "Kirby bilgi", + "metatags.license": "Kirby lisans", + "metatags.version.toolkit": "Toolkit lisans", + "metatags.version.kirby": "Kirby versiyon", + "metatags.version.panel": "Panel versiyon", + "metatags.back": "Kontrol paneline geri dön", + "metatags.files": "Site dosyaları", + "site.delete.error": "Site silinemez", + "pages.show.settings": "Sayfa ayarları", + "pages.show.preview": "Önizlemeyi aç", + "pages.show.template": "Şablon", + "pages.show.changeurl": "Web adresini değiştir", + "pages.show.invisible": "Durum: görünmez", + "pages.show.visible": "Durum: görünür", + "pages.show.changes.text": "Kaydedilmemiş değişiklikleriniz var!", + "pages.show.changes.button": "Vazgeç", + "pages.show.delete": "Bu sayfayı sil", + "pages.show.subpages.title": "Alt sayfalar", + "pages.show.subpages.edit": "Düzenle", + "pages.show.subpages.add": "Ekle", + "pages.show.subpages.empty": "Bu sayfanın şu an bir alt sayfası yok", + "pages.show.files.title": "Dosyalar", + "pages.show.files.edit": "Düzenle", + "pages.show.files.add": "Ekle", + "pages.show.files.empty": "Bu sayfanın şu an bir dosyası yok", + "pages.show.error.permissions.title": "Sayfa yazılabilir değil", + "pages.show.error.permissions.text": "Lütfen content klasörünün ve içindekilerinin izin yapılandırmasını kontrol ediniz", + "pages.show.error.permissions.retry": "Tekrar Dene", + "pages.show.error.notitle.title": "Blueprint'in başlık alanı yok", + "pages.show.error.notitle.text": "Lütfen bir başlık girin ve tekrar deneyin", + "pages.show.error.notitle.retry": "Tekrar Dene", + "pages.show.error.form": "Lütfen gerekli tüm alanları doğru bir şekilde doldurunuz", + "pages.add.title.label": "Yeni bir alt sayfa ekle", + "pages.add.title.placeholder": "Başlık", + "pages.add.url.label": "Web Adres-uzantısı", + "pages.add.url.enter": "(bir başlık yaz)", + "pages.add.url.close": "Kapat", + "pages.add.url.help": "İzin verilen karakterler: a-z küçük harfler, 0-9 ve normal kesik çizgiler", + "pages.add.template.label": "Şablon", + "pages.add.error.create": "Sayfa oluşturulamadı", + "pages.add.error.title": "Başlık bulunamadı", + "pages.add.error.template": "Şablon bulunamadı", + "pages.add.error.max.headline": "Yeni sayfalar eklemeye izin verilmedi", + "pages.add.error.max.text": "Mevcut sayfa sahip olabilecek maksimum alt sayfa sayısına erişti", + "pages.url.uid.label": "Web Adres-uzantısı", + "pages.url.uid.label.option": "Başlıktan oluştur", + "pages.url.error.exists": "Bir sayfa hali hazırda aynı web adres-uzantısına sahip", + "pages.url.error.move": "Web adres-uzantısı değiştirilemedi", + "pages.url.error.rights": "Bu sayfanın adresini değiştirilemez", + "pages.template.select.label": "Şablon", + "pages.template.warning.text": "Şablonu değiştirdiğinizde ilgili alanlar da güncellenecek", + "pages.template.warning.removed": "Kaldırılan alanlar", + "pages.template.warning.replaced": "Değiştirilen alanlar", + "pages.template.warning.added": "Eklenen alanlar", + "pages.template.error": "Bu sayfa için şablon değiştirilemez", + "pages.toggle.position": "Pozisyon", + "pages.toggle.invisible": "görünmez", + "pages.toggle.publish": "Bu sayfanın durumunu **görünür** olarak değiştirmek istediğinizden emin misiniz?", + "pages.toggle.hide": "Bu sayfanın durumunu **görünmez** olarak değiştirmek istediğinizden emin misiniz?", + "pages.toggle.error.error": "Hata sayfasının durumu değiştirilemez", + "pages.delete.headline": "Bu sayfayı silmek istediğinizden emin misiniz?", + "pages.delete.error.home.headline": "Anasayfa silinemez", + "pages.delete.error.home.text": "Anasayfayı silmeyi deniyorsunuz. Fakat bu mümkün değil, çünkü istenilmeyen etkilere neden olur.", + "pages.delete.error.error.headline": "Hata sayfası silinemedi", + "pages.delete.error.error.text": "Hata sayfasını silmeyi deniyorsunuz. Fakat bu mümkün değil, çünkü istenilmeyen etkilere neden olur.", + "pages.delete.error.children.headline": "Bu sayfa silinemedi", + "pages.delete.error.children.text": "Bu sayfa alt sayfalara sahip olduğundan dolayı silinemdi. Lütfen önce bu sayfanın alt sayfalarını siliniz.", + "pages.delete.error.blocked.headline": "Bu sayfa silinemedi", + "pages.delete.error.blocked.text": "Bu sayfa kilitli ve silinemez", + "pages.search.help": "Sayfaları web adres uzantılarına göre arayınız. Arama sonuçlarından istediğiniz sayfayı klavyenizin yukarı ve aşağı tuşları ile seçip enter tuşuna bastıktan sonra aradığınız sayfaya direk yönleneceksiniz", + "pages.search.noresults": "Herhangi bir sonuç bulunamadı. Lütfen farklı bir web adres-uzantısı ismi deneyiniz.", + "pages.error.missing": "Sayfa bulunamadı", + "subpages": "Alt Sayfalar", + "subpages.index.headline": "Alt Sayfalar:", + "subpages.index.back": "Geri", + "subpages.index.add": "Yeni bir alt sayfa ekle", + "subpages.index.add.first.text": "Bu sayfanın henüz bir alt sayfası yok", + "subpages.index.add.first.button": "İlk alt sayfanı ekle", + "subpages.index.visible": "Websitesinde gösterilecek alt sayfalar", + "subpages.index.visible.help": "Buraya websitesinde gösterilecek alt sayfaları sürükleyip bırakabilirsiniz", + "subpages.index.invisible": "Websitesinden gizlenecek alt sayfalar", + "subpages.index.invisible.help": "Buraya websitesinden gizlenecek alt sayfaları sürükleyip bırakabilirsiniz", + "subpages.add.error": "Bu sayfaya alt sayfa eklenemez", + "subpages.add.error.more": "Bu sayfaya daha fazla alt sayfa eklenemez", + "subpages.error.missing": "Sayfa bulunamadı", + "files": "Dosyalar", + "files.index.headline": "Dosyalar:", + "files.index.back": "Geri", + "files.index.upload": "Yeni bir dosya yükle", + "files.index.upload.first.text": "Bu sayfanın henüz bir dosyası yok", + "files.index.upload.first.button": "İlk dosyayı yükle", + "files.index.edit": "Düzenle", + "files.index.delete": "Sil", + "files.index.error.disabled": "Bu sayfaya dosya eklenemez", + "files.add.error.max": "Bu sayfa için maximum dosya limitine ulaşıldı", + "files.add.error.extension.missing": "Uzantısı olmayan dosya yüklenemez", + "files.add.error.extension.forbidden": "İzin verilmeyen dosya uzantısı", + "files.add.error.mime.forbidden": "İzin verilmeyen dosya tanımlayıcısı", + "files.add.error.htaccess": "htaccess dosyası yüklenemez", + "files.add.error.invisible": "Görünmez dosyalar yüklenemez", + "files.add.blueprint.type.error": "Sayfanın desteklediği formatlar:", + "files.add.blueprint.size.error": "Sayfanın izin verdiği boyut:", + "files.show.name.label": "Dosya İsmi", + "files.show.info.label": "Dosya Formatı / Büyüklüğü / Boyutları", + "files.show.link.label": "Dosyanın Web Adres Uzantısı", + "files.show.open": "Göster/indir", + "files.show.back": "Geri", + "files.show.replace": "Değiştir", + "files.show.delete": "Sil", + "files.show.error.rename": "Dosya ismi yeniden adlandırılamadı", + "files.show.error.form": "Lütfen gerekli tüm alanları doğru bir şekilde doldurunuz", + "files.upload.drop": "Dosyaları buraya sürükle bırak…", + "files.upload.click": "…veya cihazından başka bir dosya yüklemek için buraya tıkla", + "files.replace.drop": "Buraya bir dosya sürükle bırak…", + "files.replace.click": "…veya cihazındaki başka bir dosya ile değiştirmek için buraya tıkla", + "files.replace.error.type": "Yüklenecek dosyalar aynı dosya formatında olmalı", + "files.delete.headline": "Bu dosyayı silmek istediğinizden emin misiniz?", + "files.error.missing.page": "Sayfa bulunamadı", + "files.error.missing.file": "Dosya bulunamadı", + "users": "Kullanıcılar", + "users.index.headline": "Bütün kullanıcılar", + "users.index.add": "Yeni bir kullanıcı ekle", + "users.index.edit": "Düzenle", + "users.index.delete": "Sil", + "users.form.username.label": "Kullanıcı Adı", + "users.form.username.placeholder": "Kullanıcı adı seç", + "users.form.username.help": "İzin verilen karakterler: a-z küçük harfler, 0-9 ve normal kesik çizgiler", + "users.form.username.readonly": "Kullancı adı değiştirilemez", + "users.form.firstname.label": "Ad", + "users.form.lastname.label": "Soyad", + "users.form.email.label": "E-Posta", + "users.form.email.placeholder": "eposta@ornek.com", + "users.form.password.label": "Şifre", + "users.form.password.confirm.label": "Şifre Tekrarı", + "users.form.password.new.label": "Yeni Şifre", + "users.form.password.new.confirm.label": "Yeni şifre tekrarı", + "users.form.password.new.help": "Mevcut şifreyi boş bırak", + "users.form.language.label": "Dil", + "users.form.role.label": "Rol", + "users.form.options.headline": "Kullanıcı Ayarları", + "users.form.options.message": "E-Posta gönder", + "users.form.options.delete": "Bu hesabı sil", + "users.form.avatar.headline": "Profil resmi", + "users.form.avatar.upload": "Profil resmi yükle", + "users.form.avatar.replace": "Profil resmini değiştir", + "users.form.avatar.delete": "Profil resmini sil", + "users.form.back": "Kullanıcılar paneline geri dön", + "users.form.error.password.confirm": "Lütfen şifreyi doğrulayın", + "users.form.error.update": "Kullanıcı güncellenemedi", + "users.form.error.update.rights": "Bu kullanıcıyı güncellemek için yetkiniz yok", + "users.form.error.create": "Kullanıcı oluşturulamadı", + "users.form.error.permissions.title": "Account klasörü yazılabilir değil", + "users.form.error.permissions.text": "Lütfen /site/accounts klasörünün mevcut olduğundan ve yazılabilir olduğundan emin olun.", + "users.delete.headline": "Bu kullanıcıyı silmek istediğinizden emin misiniz?", + "users.delete.error": "Kullanıcı silinemedi", + "users.delete.error.permission": "Kullanıcıları silme yetkiniz yok", + "users.delete.error.permission.single": "Bu kullanıcıyı silme yetkiniz yok", + "users.delete.error.lastadmin": "Son yönetici kullanıcıyı silemezsiniz", + "users.avatar.drop": "Buraya bir profil resmi sürükle bırak…", + "users.avatar.click": "…veya cihazından başka bir profil resmi yüklemek için buraya tıkla", + "users.avatar.error.type": "Sadece JPG, PNG and GIF formatındaki dosyaları yükleyebilirsin", + "users.avatar.error.folder.headline": "Avatar klasörü yazılabilir değil", + "users.avatar.error.folder.text": "Lütfen /assets/avatars adlı klasör oluştur ve profil resmini yükleyebilmek için yazılabilir izni ver.", + "users.avatar.error.permission": "Avatarı değiştirme yetkiniz yok", + "users.avatar.delete.error": "Profil resmi silinemedi", + "users.avatar.delete.error.permission": "Bu kullanıcının avatarını silme yetkiniz yok", + "users.avatar.delete.success": "Profil resmi silindi", + "users.avatar.missing": "Bu kullanıcının avatarı yok", + "users.error.missing": "Kullanıcı bulunamadı", + "user.error.lastadmin": "Tek yönetici sensin. Bu değiştirilemez", + "form.error.missing": "Form bulunamadı", + "form.construct.error.invalid": "Geçersiz form kurucu metodu", + "fields.required": "Zorunlu", + "fields.date.label": "Tarih", + "fields.date.months": [ + "Ocak", + "Şubat", + "Mart", + "Nisan", + "Mayıs", + "Haziran", + "Temmuz", + "Ağustos", + "Eylül", + "Ekim", + "Kasım", + "Aralık" + ], + "fields.date.weekdays": [ + "Pazar", + "Pazartesi", + "Salı", + "Çarşamba", + "Perşembe", + "Cuma", + "Cumartesi" + ], + "fields.date.weekdays.short": [ + "Paz", + "Pzt", + "Sal", + "Çar", + "Per", + "Cum", + "Cmt" + ], + "fields.email.label": "E-Posta", + "fields.email.placeholder": "eposta@ornek.com", + "fields.number.label": "Numara", + "fields.number.placeholder": "#", + "fields.page.label": "Sayfa", + "fields.page.placeholder": "adres/yolu/sayfa", + "fields.password.label": "Şifre", + "fields.structure.add": "Ekle", + "fields.structure.add.first": "İlk girdini ekle", + "fields.structure.empty": "Henüz bir girdi yok", + "fields.structure.entry.error": "Öğe bulunamadı", + "fields.structure.cancel": "İptal", + "fields.structure.save": "Kaydet", + "fields.structure.edit": "Düzenle", + "fields.structure.delete": "Sil", + "fields.structure.delete.label": "Bu girdiyi silmek istediğinizden emin misiniz?", + "fields.tags.label": "Anahtar Kelimeler", + "fields.tel.label": "Telefon", + "fields.textarea.buttons.bold.label": "Kalın yazı", + "fields.textarea.buttons.bold.text": "Kalın yazı", + "fields.textarea.buttons.italic.label": "Eğik yazı", + "fields.textarea.buttons.italic.text": "Eğik yazı", + "fields.textarea.buttons.link.label": "Bağlantı", + "fields.textarea.buttons.email.label": "E-Posta", + "fields.textarea.buttons.image.label": "Resim", + "fields.textarea.buttons.file.label": "Dosya", + "fields.toggle.yes": "Evet", + "fields.toggle.no": "Hayır", + "fields.toggle.on": "Açık", + "fields.toggle.off": "Kapalı", + "fields.error.missing.controller": "Alan denetleyici dosyası eksik", + "fields.error.missing.class": "Alan kontrol sınıfı eksik", + "fields.error.route.invalid": "Geçersiz alan rotası", + "fields.error.extended": "Bu alan genişletilemez", + "editor.link.url.label": "Web Adresi Ekle", + "editor.link.text.label": "Bağlantı yazısı", + "editor.link.text.help": "Bağlantı yazısı isteğe bağlı", + "editor.email.address.label": "E-Posta Adresi Ekle", + "editor.email.address.placeholder": "eposta@ornek.com", + "editor.email.text.label": "Bağlantı yazısı", + "editor.email.text.help": "Bağlantı yazısı isteğe bağlı", + "editor.file.empty": "Bu sayfanın henüz bir dosyası yok", + "editor.image.empty": "Bu sayfanın henüz bir resmi yok", + "autocomplete.method.error": "Geçersiz otomatik doldurma metodu", + "blueprints.error.default.missing": "Varsayılan taslak dosyası eksik", + "error": "Hata", + "error.headline": "Hata" +} \ No newline at end of file diff --git a/panel/app/translations/tr/package.json b/panel/app/translations/tr/package.json new file mode 100644 index 0000000..1be89b1 --- /dev/null +++ b/panel/app/translations/tr/package.json @@ -0,0 +1,4 @@ +{ + "title": "Türkçe", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/zh_CN/core.json b/panel/app/translations/zh_CN/core.json new file mode 100644 index 0000000..dd52dec --- /dev/null +++ b/panel/app/translations/zh_CN/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "取消", + "add": "新增", + "addit": "新增并编辑", + "save": "保存", + "saved": "已保存!", + "change": "更改", + "delete": "删除", + "insert": "插入", + "ok": "确认", + "routes.error.invalid": "无效的控制面板URL", + "controller.error.invalid": "无效的控制器", + "controller.error.action": "无效行为", + "view.error.invalid": "无效视图:", + "options.show": "显示选项", + "options.hide": "隐藏选项", + "installation": "安装", + "installation.check.headline": "安装Kirby控制面板", + "installation.check.text": "Kirby在安装过程中遇到以下问题...", + "installation.check.retry": "重试", + "installation.check.error": "存在一些问题!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts不可写入", + "installation.check.error.avatars": "/assets/avatars不可写入", + "installation.check.error.blueprints": "请新增/site/blueprints文件夹", + "installation.check.error.content": "content文件夹及其内容必须可写入", + "installation.check.error.thumbs": "thumbs文件夹必须可写入", + "installation.signup.username.label": "创建首个账户", + "installation.signup.username.placeholder": "用户名", + "installation.signup.email.label": "邮箱", + "installation.signup.email.placeholder": "mail@example.com", + "installation.signup.password.label": "密码", + "installation.signup.language.label": "语言", + "installation.signup.button": "创建账户", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "用户名", + "login.password.label": "密码", + "login.error": "无效的用户名或密码", + "login.button": "Log in", + "login.log.error.permissions": "登陆的log file不可写入", + "logout": "Log out", + "topbar.error.class.definition": "Missing topbar definition for class:", + "dashboard": "仪表盘", + "dashboard.index.pages.title": "页面", + "dashboard.index.pages.edit": "编辑", + "dashboard.index.pages.add": "新增", + "dashboard.index.site.title": "网址", + "dashboard.index.account.title": "您的账户", + "dashboard.index.account.edit": "编辑", + "dashboard.index.metatags.title": "网站选项", + "dashboard.index.metatags.edit": "编辑", + "dashboard.index.history.title": "最近更新", + "dashboard.index.history.text": "您最近修改的页面会显示在这里,以便查找。", + "dashboard.index.license.title": "Kirby", + "dashboard.index.license.text": "好像您在一个公共服务器上运行没有许可证的Kirby!\n请支持一下Kirby,购买一个许可证。\n如果您已有许可证号,请把它添加到config文件中。", + "metatags": "网站选项", + "metatags.info": "Kirby信息", + "metatags.license": "Kirby", + "metatags.version.toolkit": "工具箱版本", + "metatags.version.kirby": "Kirby版本", + "metatags.version.panel": "控制面板版本", + "metatags.back": "返回控制面板", + "metatags.files": "网站文件", + "site.delete.error": "该网站不能被删除", + "pages.show.settings": "页面设置", + "pages.show.preview": "打开预览", + "pages.show.template": "模版", + "pages.show.changeurl": "更改URL", + "pages.show.invisible": "状态:不可见", + "pages.show.visible": "状态:可见", + "pages.show.changes.text": "您有未保存的修改!", + "pages.show.changes.button": "放弃", + "pages.show.delete": "删除此页", + "pages.show.subpages.title": "页面", + "pages.show.subpages.edit": "编辑", + "pages.show.subpages.add": "新增", + "pages.show.subpages.empty": "此页面没有子页面", + "pages.show.files.title": "文件", + "pages.show.files.edit": "编辑", + "pages.show.files.add": "新增", + "pages.show.files.empty": "此页面没有文件", + "pages.show.error.permissions.title": "此页面不可写入", + "pages.show.error.permissions.text": "请检查content文件夹和所有文件的修改权限", + "pages.show.error.permissions.retry": "重试", + "pages.show.error.notitle.title": "该blueprint没有标题栏", + "pages.show.error.notitle.text": "请新增标题栏并重试", + "pages.show.error.notitle.retry": "重试", + "pages.show.error.form": "请正确填写所有栏", + "pages.add.title.label": "新增页面", + "pages.add.title.placeholder": "标题", + "pages.add.url.label": "URL后缀", + "pages.add.url.enter": "(输入标题)", + "pages.add.url.close": "关闭", + "pages.add.url.help": "格式要求:小写字母a-z、数字0-9及连字符-", + "pages.add.template.label": "模版", + "pages.add.error.create": "无法新增页面", + "pages.add.error.title": "标题缺失", + "pages.add.error.template": "模版缺失", + "pages.add.error.max.headline": "新页面不允许被创建", + "pages.add.error.max.text": "当前页面的子页面数量已达上限", + "pages.url.uid.label": "URL后缀", + "pages.url.uid.label.option": "从标题创建", + "pages.url.error.exists": "含有相同后缀的页面已存在", + "pages.url.error.move": "该后缀不能被修改", + "pages.url.error.rights": "您不能更改该页面的URL", + "pages.template.select.label": "模版", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "位置", + "pages.toggle.invisible": "隐藏", + "pages.toggle.publish": "确定更改该页面的状态为可见状态?", + "pages.toggle.hide": "确定更改该页面的状态为不可见状态?", + "pages.toggle.error.error": "error页面的状态不能更改", + "pages.delete.headline": "确定删除该页面?", + "pages.delete.error.home.headline": "home页面不能被删除", + "pages.delete.error.home.text": "您正在尝试删除home页面。", + "pages.delete.error.error.headline": "error页面不能删除", + "pages.delete.error.error.text": "您正在尝试删除error页面,该行为不被允许,否则会产生不利后果。", + "pages.delete.error.children.headline": "该页面不能删除", + "pages.delete.error.children.text": "该页含有子页面,不可删除。请先删除子页面。", + "pages.delete.error.blocked.headline": "该页面不能删除", + "pages.delete.error.blocked.text": "该页面已锁定,不可删除", + "pages.search.help": "通过URL搜索页面。使用上下键进行选择,按回车键进入相应页面。", + "pages.search.noresults": "当前请求没有搜索结果。请重试。", + "pages.error.missing": "该页面找不到", + "subpages": "页面", + "subpages.index.headline": "Pages in", + "subpages.index.back": "返回", + "subpages.index.add": "新增页面", + "subpages.index.add.first.text": "该页面还没有子页面", + "subpages.index.add.first.button": "添加首个页面", + "subpages.index.visible": "可见页面", + "subpages.index.visible.help": "拖拽隐藏页面到这里以排序/使其可见", + "subpages.index.invisible": "隐藏页面", + "subpages.index.invisible.help": "拖拽可见页面到这里以撤销分类/使其隐藏", + "subpages.add.error": "该页面没有拥有子页面权限", + "subpages.add.error.more": "该页面不能再添加子页面。", + "subpages.error.missing": "该页面找不到", + "files": "文件", + "files.index.headline": "Files for", + "files.index.back": "返回", + "files.index.upload": "上传新文件", + "files.index.upload.first.text": "该页面还没有文件", + "files.index.upload.first.button": "上传首份文件", + "files.index.edit": "编辑", + "files.index.delete": "删除", + "files.index.error.disabled": "该页面不允许包含任何文件", + "files.add.error.max": "当前页面的文件数量已达上限", + "files.add.error.extension.missing": "您不可上传没有扩展名的文件", + "files.add.error.extension.forbidden": "禁用文件扩展名", + "files.add.error.mime.forbidden": "禁用MIME类型", + "files.add.error.htaccess": "htaccess文件不能被上传", + "files.add.error.invisible": "隐藏页面不能被上传", + "files.add.blueprint.type.error": "该页只允许:", + "files.add.blueprint.size.error": "页面只允许文件大小为", + "files.show.name.label": "文件名", + "files.show.info.label": "类型/大小/尺寸", + "files.show.link.label": "公共链接", + "files.show.open": "显示/下载文件", + "files.show.back": "返回", + "files.show.replace": "替换", + "files.show.delete": "删除", + "files.show.error.rename": "该文件不能被重命名", + "files.show.error.form": "请正确填写所有栏", + "files.upload.drop": "拖拽文件到这里...", + "files.upload.click": "或点击来上传", + "files.replace.drop": "拖拽文件到这里...", + "files.replace.click": "...或点击来替换", + "files.replace.error.type": "上传文件必须是同类型", + "files.delete.headline": "您确定删除这个文件?", + "files.error.missing.page": "该页面找不到", + "files.error.missing.file": "文件找不到", + "users": "用户", + "users.index.headline": "所有用户", + "users.index.add": "添加新用户", + "users.index.edit": "编辑", + "users.index.delete": "删除", + "users.form.username.label": "用户名", + "users.form.username.placeholder": "您的用户名", + "users.form.username.help": "允许字符:小写字母[a-z]和连字符[-]", + "users.form.username.readonly": "用户名不可更改", + "users.form.firstname.label": "名", + "users.form.lastname.label": "姓", + "users.form.email.label": "邮箱", + "users.form.email.placeholder": "mail@example.com", + "users.form.password.label": "密码", + "users.form.password.confirm.label": "确认密码", + "users.form.password.new.label": "新密码", + "users.form.password.new.confirm.label": "确认新密码", + "users.form.password.new.help": "保留此处空白以保留当前密码", + "users.form.language.label": "语言", + "users.form.role.label": "角色", + "users.form.options.headline": "账户选项", + "users.form.options.message": "发送邮件", + "users.form.options.delete": "删除帐户", + "users.form.avatar.headline": "用户资料图片", + "users.form.avatar.upload": "上传用户资料图片", + "users.form.avatar.replace": "替换用户资料图片", + "users.form.avatar.delete": "删除用户资料图片", + "users.form.back": "回到用户", + "users.form.error.password.confirm": "请核实密码", + "users.form.error.update": "无法更新该用户", + "users.form.error.update.rights": "您没有更新该用户权限", + "users.form.error.create": "该用户无法创建", + "users.form.error.permissions.title": "账户文件夹不可写入", + "users.form.error.permissions.text": "请确保/site/accounts 文件夹存在且可写入", + "users.delete.headline": "确定删除该用户?", + "users.delete.error": "该用户无法删除", + "users.delete.error.permission": "您没有删除用户权限", + "users.delete.error.permission.single": "您没有删除该用户权限", + "users.delete.error.lastadmin": "您不能删除仅有的admin账户", + "users.avatar.drop": "拖拽用户资料图片到这里...", + "users.avatar.click": "或点击来上传", + "users.avatar.error.type": "只能上传JPG、PNG或GIF格式文件", + "users.avatar.error.folder.headline": "avatar文件夹不可写入", + "users.avatar.error.folder.text": "请新建/assets/avatats文件夹并使其可写入,以上传用户资料图片。", + "users.avatar.error.permission": "您没有修改头像的权限", + "users.avatar.delete.error": "该用户资料图片不能被删除。", + "users.avatar.delete.error.permission": "您没有删除这个用户头像的权限", + "users.avatar.delete.success": "该用户资料图片已删除。", + "users.avatar.missing": "该用户没有头像", + "users.error.missing": "该用户找不到", + "user.error.lastadmin": "您是仅有的admin用户,不可更改。", + "form.error.missing": "表单不存在", + "form.construct.error.invalid": "Invalid form construction method", + "fields.required": "必填", + "fields.date.label": "日期", + "fields.date.months": [ + "一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月" + ], + "fields.date.weekdays": [ + "周日", + "周一", + "周二", + "周三", + "周四", + "周五", + "周六" + ], + "fields.date.weekdays.short": [ + "周日", + "周一", + "周二", + "周三", + "周四", + "周五", + "周六" + ], + "fields.email.label": "邮箱", + "fields.email.placeholder": "mail@example.com", + "fields.number.label": "数字", + "fields.number.placeholder": "#", + "fields.page.label": "页面", + "fields.page.placeholder": "path/to/page", + "fields.password.label": "密码", + "fields.structure.add": "新增", + "fields.structure.add.first": "Add the first entry", + "fields.structure.empty": "No entries yet.", + "fields.structure.entry.error": "该条目无法找到", + "fields.structure.cancel": "取消", + "fields.structure.save": "确认", + "fields.structure.edit": "编辑", + "fields.structure.delete": "删除", + "fields.structure.delete.label": "确定删除这项纪录?", + "fields.tags.label": "标签", + "fields.tel.label": "电话", + "fields.textarea.buttons.bold.label": "粗体文本", + "fields.textarea.buttons.bold.text": "粗体文本", + "fields.textarea.buttons.italic.label": "斜体文本", + "fields.textarea.buttons.italic.text": "斜体文本", + "fields.textarea.buttons.link.label": "链接", + "fields.textarea.buttons.email.label": "邮箱", + "fields.textarea.buttons.image.label": "图像", + "fields.textarea.buttons.file.label": "文件", + "fields.toggle.yes": "是", + "fields.toggle.no": "否", + "fields.toggle.on": "开启", + "fields.toggle.off": "关闭", + "fields.error.missing.controller": "The field controller file is missing", + "fields.error.missing.class": "The field controller class is missing", + "fields.error.route.invalid": "Invalid field route", + "fields.error.extended": "The field cannot be extended", + "editor.link.url.label": "插入URL", + "editor.link.text.label": "链接文本", + "editor.link.text.help": "链接文本可选", + "editor.email.address.label": "插入邮箱地址", + "editor.email.address.placeholder": "mail@example.com", + "editor.email.text.label": "链接文本", + "editor.email.text.help": "链接文本可选", + "editor.file.empty": "该页面没有文件", + "editor.image.empty": "该页没有图像", + "autocomplete.method.error": "无效的自动完成方法", + "blueprints.error.default.missing": "默认blueprint缺失", + "error": "错误", + "error.headline": "错误" +} \ No newline at end of file diff --git a/panel/app/translations/zh_CN/package.json b/panel/app/translations/zh_CN/package.json new file mode 100644 index 0000000..52082f2 --- /dev/null +++ b/panel/app/translations/zh_CN/package.json @@ -0,0 +1,4 @@ +{ + "title": "简体中文(大陆)‎", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/zh_TW/core.json b/panel/app/translations/zh_TW/core.json new file mode 100644 index 0000000..1f1e082 --- /dev/null +++ b/panel/app/translations/zh_TW/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "取消", + "add": "新增", + "addit": "Add & Edit", + "save": "儲存", + "saved": "已儲存", + "change": "Change", + "delete": "刪除", + "insert": "插入", + "ok": "好", + "routes.error.invalid": "Invalid Panel URL", + "controller.error.invalid": "Invalid controller", + "controller.error.action": "Invalid action", + "view.error.invalid": "Invalid view:", + "options.show": "顯示選項", + "options.hide": "隱藏選項", + "installation": "安裝", + "installation.check.headline": "安裝Kirby控制台", + "installation.check.text": "Kirby在安裝過程中遇到以下問題…", + "installation.check.retry": "重試", + "installation.check.error": "這裡發生了一些問題!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "您沒有「/site/accounts」資料夾的修改權限", + "installation.check.error.avatars": "您沒有「/assets/avatars」資料夾的修改權限", + "installation.check.error.blueprints": "請新增「/site/blueprints」資料夾", + "installation.check.error.content": "您沒有「/content」資料夾的修改權限", + "installation.check.error.thumbs": "您沒有「/thumbs」資料夾的修改權限", + "installation.signup.username.label": "建立首位使用者", + "installation.signup.username.placeholder": "帳號", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@example.com", + "installation.signup.password.label": "密碼", + "installation.signup.language.label": "慣用語言", + "installation.signup.button": "建立", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "帳號", + "login.password.label": "密碼", + "login.error": "帳號與密碼不正確!", + "login.button": "Log in", + "login.log.error.permissions": "Login log file is not writable.", + "logout": "Log out", + "topbar.error.class.definition": "Missing topbar definition for class:", + "dashboard": "控制台", + "dashboard.index.pages.title": "頁面", + "dashboard.index.pages.edit": "編輯", + "dashboard.index.pages.add": "新增", + "dashboard.index.site.title": "網址", + "dashboard.index.account.title": "您的帳號", + "dashboard.index.account.edit": "編輯", + "dashboard.index.metatags.title": "網站資料", + "dashboard.index.metatags.edit": "編輯", + "dashboard.index.history.title": "頁面編輯紀錄", + "dashboard.index.history.text": "您最近修改的頁面會顯示在這裡,方便您再次編輯。", + "dashboard.index.license.title": "Kirby license", + "dashboard.index.license.text": "It seems you are running Kirby on a public server without a valid license!\n\nPlease, support Kirby and (link: {buy} text: buy a license now)\n\nIf you already have a license key, just add it to your config file: (link: {docs} text: site/config/config.php)", + "metatags": "網站資料", + "metatags.info": "Kirby info", + "metatags.license": "Kirby license", + "metatags.version.toolkit": "Toolkit version", + "metatags.version.kirby": "Kirby version", + "metatags.version.panel": "Panel version", + "metatags.back": "返回控制台", + "metatags.files": "Site files", + "site.delete.error": "The site cannot be deleted", + "pages.show.settings": "頁面設定", + "pages.show.preview": "在新分頁中瀏覽此頁面", + "pages.show.template": "頁面模板", + "pages.show.changeurl": "更改頁面網址", + "pages.show.invisible": "Status: invisible", + "pages.show.visible": "Status: visible", + "pages.show.changes.text": "You have unsaved changes!", + "pages.show.changes.button": "Discard", + "pages.show.delete": "刪除此頁面", + "pages.show.subpages.title": "子頁面", + "pages.show.subpages.edit": "編輯", + "pages.show.subpages.add": "新增", + "pages.show.subpages.empty": "目前沒有子頁面", + "pages.show.files.title": "附加檔案", + "pages.show.files.edit": "編輯", + "pages.show.files.add": "上傳", + "pages.show.files.empty": "目前沒有附加檔案", + "pages.show.error.permissions.title": "您沒有修改此頁面的權限", + "pages.show.error.permissions.text": "請確認「/content」資料夾的修改權限", + "pages.show.error.permissions.retry": "重試", + "pages.show.error.notitle.title": "模板的「blueprint」中沒有「title」欄位", + "pages.show.error.notitle.text": "請在新增「title」欄位後重試", + "pages.show.error.notitle.retry": "重試", + "pages.show.error.form": "請正確填入所有欄位", + "pages.add.title.label": "新增頁面", + "pages.add.title.placeholder": "頁面標題", + "pages.add.url.label": "頁面網址", + "pages.add.url.enter": "(從頁面標題輸入)", + "pages.add.url.close": "關閉", + "pages.add.url.help": "可用字元:英文小寫 a-z、數字 0-9以及分號「-」", + "pages.add.template.label": "頁面模板", + "pages.add.error.create": "The page could not be created", + "pages.add.error.title": "沒有頁面標題", + "pages.add.error.template": "沒有頁面模板", + "pages.add.error.max.headline": "無法新增頁面", + "pages.add.error.max.text": "子頁面數量已達上限", + "pages.url.uid.label": "頁面網址", + "pages.url.uid.label.option": "從頁面標題輸入", + "pages.url.error.exists": "已有重複的頁面網址", + "pages.url.error.move": "無法更改頁面網址", + "pages.url.error.rights": "You cannot change the URL of this page", + "pages.template.select.label": "頁面模板", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Position", + "pages.toggle.invisible": "invisible", + "pages.toggle.publish": "Do you really want to change the status of this page to **visible?**", + "pages.toggle.hide": "Do you really want to change the status of this page to **invisible?**", + "pages.toggle.error.error": "The status of the error page cannot be changed", + "pages.delete.headline": "確認刪除此頁面?", + "pages.delete.error.home.headline": "不可刪除首頁", + "pages.delete.error.home.text": "如果刪除首頁將導致不可預期的後果。", + "pages.delete.error.error.headline": "不可刪除出錯頁面", + "pages.delete.error.error.text": "如果刪除出錯頁面將無法重新引導訪客。", + "pages.delete.error.children.headline": "無法刪除頁面", + "pages.delete.error.children.text": "此頁面底下有其他子頁面,請先刪除所有子頁面。", + "pages.delete.error.blocked.headline": "頁面不可被刪除", + "pages.delete.error.blocked.text": "此為保護頁面", + "pages.search.help": "透過頁面網址搜尋(可用上下鍵選擇並按 Enter 鍵進入)", + "pages.search.noresults": "沒有符合關鍵字的搜尋結果", + "pages.error.missing": "找不到頁面", + "subpages": "頁面", + "subpages.index.headline": "所有子頁面 in", + "subpages.index.back": "返回", + "subpages.index.add": "新增頁面", + "subpages.index.add.first.text": "目前沒有子頁面", + "subpages.index.add.first.button": "新增", + "subpages.index.visible": "可見頁面", + "subpages.index.visible.help": "可拖放隱藏頁面到這裡", + "subpages.index.invisible": "隱藏頁面", + "subpages.index.invisible.help": "可拖放頁面到這裡將它們隱藏", + "subpages.add.error": "This page is not allowed to have subpages", + "subpages.add.error.more": "This page cannot have any more subpages", + "subpages.error.missing": "找不到頁面", + "files": "附加檔案", + "files.index.headline": "附加檔案 for", + "files.index.back": "返回", + "files.index.upload": "上傳附加檔案", + "files.index.upload.first.text": "目前沒有附加檔案", + "files.index.upload.first.button": "上傳", + "files.index.edit": "編輯", + "files.index.delete": "刪除", + "files.index.error.disabled": "The page is not allowed to have any files", + "files.add.error.max": "The maximum number of files for the current page has been reached.", + "files.add.error.extension.missing": "You cannot upload files without extension", + "files.add.error.extension.forbidden": "Forbidden file extension", + "files.add.error.mime.forbidden": "Forbidden mime type", + "files.add.error.htaccess": "htaccess files cannot be uploaded", + "files.add.error.invisible": "Invisible files cannot be uploaded", + "files.add.blueprint.type.error": "Page only allows:", + "files.add.blueprint.size.error": "Page only allows file size of", + "files.show.name.label": "檔案名稱", + "files.show.info.label": "檔案格式 / 大小 / 尺寸", + "files.show.link.label": "公開連結", + "files.show.open": "開啟/下載", + "files.show.back": "返回", + "files.show.replace": "更換", + "files.show.delete": "刪除", + "files.show.error.rename": "無法重新命名檔案", + "files.show.error.form": "請正確填入所有欄位", + "files.upload.drop": "請拖放檔案到這裡", + "files.upload.click": "...或按此上傳", + "files.replace.drop": "請拖放檔案到這裡", + "files.replace.click": "...或按此上傳", + "files.replace.error.type": "必須上傳相同格式的檔案", + "files.delete.headline": "確認刪除檔案?", + "files.error.missing.page": "找不到頁面", + "files.error.missing.file": "找不到檔案", + "users": "使用者", + "users.index.headline": "所有使用者", + "users.index.add": "新增使用者", + "users.index.edit": "編輯", + "users.index.delete": "刪除", + "users.form.username.label": "帳號", + "users.form.username.placeholder": "帳號", + "users.form.username.help": "可用字元:英文小寫 a-z、數字 0-9以及分號「-」", + "users.form.username.readonly": "帳號不可更改", + "users.form.firstname.label": "名字", + "users.form.lastname.label": "姓氏", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@example.com", + "users.form.password.label": "密碼", + "users.form.password.confirm.label": "再次確認密碼", + "users.form.password.new.label": "更改密碼", + "users.form.password.new.confirm.label": "再次確認密碼", + "users.form.password.new.help": "若要更改密碼請填入新密碼", + "users.form.language.label": "慣用語言", + "users.form.role.label": "權限", + "users.form.options.headline": "使用者選項", + "users.form.options.message": "傳送郵件", + "users.form.options.delete": "刪除使用者", + "users.form.avatar.headline": "使用者照片", + "users.form.avatar.upload": "上傳使用者照片", + "users.form.avatar.replace": "更換使用者照片", + "users.form.avatar.delete": "刪除使用者照片", + "users.form.back": "返回使用者名單", + "users.form.error.password.confirm": "請確認密碼無誤", + "users.form.error.update": "無法更改使用者", + "users.form.error.update.rights": "You are not allowed to update this user", + "users.form.error.create": "無法新增使用者", + "users.form.error.permissions.title": "您沒有「/site/accounts」資料夾的修改權限", + "users.form.error.permissions.text": "請確認「/site/accounts」資料夾是否存在並具有修改權限", + "users.delete.headline": "確認刪除使用者?", + "users.delete.error": "該使用者不可被刪除", + "users.delete.error.permission": "You are not allowed to delete users", + "users.delete.error.permission.single": "You are not allowed to delete this user", + "users.delete.error.lastadmin": "You cannot delete the last admin", + "users.avatar.drop": "請拖放照片到這裡", + "users.avatar.click": "...或按此上傳", + "users.avatar.error.type": "只能使用 JPG、PNG 和 GIF 格式的圖片", + "users.avatar.error.folder.headline": "您沒有「/assets/avatars」資料夾的修改權限", + "users.avatar.error.folder.text": "請確認「/assets/avatars」資料夾是否存在並具有修改權限", + "users.avatar.error.permission": "You are not allowed to change the avatar", + "users.avatar.delete.error": "無法刪除使用者照片", + "users.avatar.delete.error.permission": "You are not allowed to delete the avatar of this user", + "users.avatar.delete.success": "已刪除使用者照片", + "users.avatar.missing": "This user has no avatar", + "users.error.missing": "找不到使用者", + "user.error.lastadmin": "You are the only admin. This cannot be changed.", + "form.error.missing": "The form cannot be found", + "form.construct.error.invalid": "Invalid form construction method", + "fields.required": "必要欄位", + "fields.date.label": "日期", + "fields.date.months": [ + "一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月" + ], + "fields.date.weekdays": [ + "週日", + "週一", + "週二", + "週三", + "週四", + "週五", + "週六" + ], + "fields.date.weekdays.short": [ + "日", + "一", + "二", + "三", + "四", + "五", + "六" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@example.com", + "fields.number.label": "數字", + "fields.number.placeholder": "#", + "fields.page.label": "頁面", + "fields.page.placeholder": "頁面路徑", + "fields.password.label": "密碼", + "fields.structure.add": "新增", + "fields.structure.add.first": "新增第一筆資料", + "fields.structure.empty": "還沒有資料", + "fields.structure.entry.error": "The item could not be found", + "fields.structure.cancel": "取消", + "fields.structure.save": "儲存", + "fields.structure.edit": "編輯", + "fields.structure.delete": "刪除", + "fields.structure.delete.label": "Do you really want to delete this entry?", + "fields.tags.label": "標籤", + "fields.tel.label": "電話", + "fields.textarea.buttons.bold.label": "粗體", + "fields.textarea.buttons.bold.text": "粗體", + "fields.textarea.buttons.italic.label": "斜體", + "fields.textarea.buttons.italic.text": "斜體", + "fields.textarea.buttons.link.label": "連結", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "圖片", + "fields.textarea.buttons.file.label": "檔案", + "fields.toggle.yes": "是", + "fields.toggle.no": "否", + "fields.toggle.on": "開啟", + "fields.toggle.off": "關閉", + "fields.error.missing.controller": "The field controller file is missing", + "fields.error.missing.class": "The field controller class is missing", + "fields.error.route.invalid": "Invalid field route", + "fields.error.extended": "The field cannot be extended", + "editor.link.url.label": "插入網址連結", + "editor.link.text.label": "連結文字", + "editor.link.text.help": "連結文字可選填", + "editor.email.address.label": "插入 Email 連結", + "editor.email.address.placeholder": "mail@example.com", + "editor.email.text.label": "連結文字", + "editor.email.text.help": "連結文字可選填", + "editor.file.empty": "無可用附加檔案", + "editor.image.empty": "無可用附加圖片檔案", + "autocomplete.method.error": "Invalid autocomplete method", + "blueprints.error.default.missing": "Missing default blueprint", + "error": "錯誤", + "error.headline": "錯誤" +} \ No newline at end of file diff --git a/panel/app/translations/zh_TW/package.json b/panel/app/translations/zh_TW/package.json new file mode 100644 index 0000000..7b5d8f1 --- /dev/null +++ b/panel/app/translations/zh_TW/package.json @@ -0,0 +1,4 @@ +{ + "title": "繁體中文(台灣)‎", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/views/auth/block.php b/panel/app/views/auth/block.php new file mode 100644 index 0000000..043e05a --- /dev/null +++ b/panel/app/views/auth/block.php @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/panel/app/views/auth/error.php b/panel/app/views/auth/error.php new file mode 100644 index 0000000..17804a9 --- /dev/null +++ b/panel/app/views/auth/error.php @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/panel/app/views/auth/login.php b/panel/app/views/auth/login.php new file mode 100644 index 0000000..0bebad8 --- /dev/null +++ b/panel/app/views/auth/login.php @@ -0,0 +1,19 @@ + + + \ No newline at end of file diff --git a/panel/app/views/avatars/delete.php b/panel/app/views/avatars/delete.php new file mode 100644 index 0000000..fd34175 --- /dev/null +++ b/panel/app/views/avatars/delete.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/panel/app/views/dashboard/index.php b/panel/app/views/dashboard/index.php new file mode 100644 index 0000000..2ffb4d5 --- /dev/null +++ b/panel/app/views/dashboard/index.php @@ -0,0 +1,49 @@ +
    + +
    + + $widget): ?> + +
    + +

    + + + + href=""> + + + + + + + + + + + + + + + + data-shortcut="" href=""> + + href=""> + + + + + + + + +

    + + + +
    + + +
    + +
    \ No newline at end of file diff --git a/panel/app/views/error/index.php b/panel/app/views/error/index.php new file mode 100644 index 0000000..d9be728 --- /dev/null +++ b/panel/app/views/error/index.php @@ -0,0 +1,4 @@ +
    +

    +

    +
    \ No newline at end of file diff --git a/panel/app/views/error/modal.php b/panel/app/views/error/modal.php new file mode 100644 index 0000000..681eedf --- /dev/null +++ b/panel/app/views/error/modal.php @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/panel/app/views/files/delete.php b/panel/app/views/files/delete.php new file mode 100644 index 0000000..fd34175 --- /dev/null +++ b/panel/app/views/files/delete.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/panel/app/views/files/edit.php b/panel/app/views/files/edit.php new file mode 100644 index 0000000..57e4370 --- /dev/null +++ b/panel/app/views/files/edit.php @@ -0,0 +1,88 @@ +
    + +
    + + + + + extension() == 'svg'): ?> + + canHavePreview()): ?> + <?php __($file->filename()) ?> + + + filename()) ?> + type() . ' / ' . $file->niceSize()) ?> + + + + +
    + + + +
    + + + + \ No newline at end of file diff --git a/panel/app/views/files/index.php b/panel/app/views/files/index.php new file mode 100644 index 0000000..23068be --- /dev/null +++ b/panel/app/views/files/index.php @@ -0,0 +1,104 @@ +
    + +

    + + isSite()): ?> + + + title()) ?> + + ( count() ?> ) + + + + + + + + hasFiles() and $page->canHaveMoreFiles()): ?> + + + + + +

    + + count()): ?> + + + + +
    +
    +

    + canHaveMoreFiles()) : ?> + + + + +
    +
    + + + +
    + + + + \ No newline at end of file diff --git a/panel/app/views/installation/index.php b/panel/app/views/installation/index.php new file mode 100644 index 0000000..41e2725 --- /dev/null +++ b/panel/app/views/installation/index.php @@ -0,0 +1,23 @@ + + + \ No newline at end of file diff --git a/panel/app/views/options/index.php b/panel/app/views/options/index.php new file mode 100644 index 0000000..30e48f9 --- /dev/null +++ b/panel/app/views/options/index.php @@ -0,0 +1,62 @@ +
    + + + +
    +
    + +
    +
    + +
    + + \ No newline at end of file diff --git a/panel/app/views/pages/add.php b/panel/app/views/pages/add.php new file mode 100644 index 0000000..9299152 --- /dev/null +++ b/panel/app/views/pages/add.php @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/panel/app/views/pages/delete.php b/panel/app/views/pages/delete.php new file mode 100644 index 0000000..fd34175 --- /dev/null +++ b/panel/app/views/pages/delete.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/panel/app/views/pages/edit.php b/panel/app/views/pages/edit.php new file mode 100644 index 0000000..d8c497f --- /dev/null +++ b/panel/app/views/pages/edit.php @@ -0,0 +1,31 @@ +
    + + + +
    +
    + + isWritable()): ?> +
    +

    + +

    +
    +

    +
    +
    + + + +
    +
    + + + + +
    +
    + +
    + + \ No newline at end of file diff --git a/panel/app/views/pages/template.php b/panel/app/views/pages/template.php new file mode 100644 index 0000000..21d06e5 --- /dev/null +++ b/panel/app/views/pages/template.php @@ -0,0 +1,21 @@ + + \ No newline at end of file diff --git a/panel/app/views/pages/toggle.php b/panel/app/views/pages/toggle.php new file mode 100644 index 0000000..fd34175 --- /dev/null +++ b/panel/app/views/pages/toggle.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/panel/app/views/pages/url.php b/panel/app/views/pages/url.php new file mode 100644 index 0000000..e9e1dc3 --- /dev/null +++ b/panel/app/views/pages/url.php @@ -0,0 +1,33 @@ + + + \ No newline at end of file diff --git a/panel/app/views/search/results.php b/panel/app/views/search/results.php new file mode 100644 index 0000000..5338d9b --- /dev/null +++ b/panel/app/views/search/results.php @@ -0,0 +1,35 @@ +count()): ?> +
    + +
    + + +count()): ?> +
    + +
    + \ No newline at end of file diff --git a/panel/app/views/subpages/index.php b/panel/app/views/subpages/index.php new file mode 100644 index 0000000..8af0578 --- /dev/null +++ b/panel/app/views/subpages/index.php @@ -0,0 +1,157 @@ +
    +

    + + isSite()): ?> + + + title()) ?> + + + + + + + + + children()->count()): ?> + + + + + + + +

    + + hasChildren()): ?> +
    + +
    +

    + + ( total() ?> ) + +

    + +
    +
    + pages() as $subpage): ?> + $page, 'subpage' => $subpage)) ?> + +
    +
    + + pagination() ?> + + total()): ?> +
    + +
    + + +
    +

    + + ( total() ?> ) + +

    + +
    +
    + + pages() as $subpage): ?> + $page, 'subpage' => $subpage)) ?> + + +
    +
    + + pagination() ?> + + total()): ?> +
    + +
    + + +
    + +
    + + + + + +
    +
    +

    + + + +
    +
    + + + + +
    + + \ No newline at end of file diff --git a/panel/app/views/users/delete.php b/panel/app/views/users/delete.php new file mode 100644 index 0000000..fd34175 --- /dev/null +++ b/panel/app/views/users/delete.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/panel/app/views/users/edit.php b/panel/app/views/users/edit.php new file mode 100644 index 0000000..06b643e --- /dev/null +++ b/panel/app/views/users/edit.php @@ -0,0 +1,108 @@ +
    + + + +
    +
    + +
    +

    + +

    +
    +

    +
    +
    +
    + + + +
    +
    + +
    + + \ No newline at end of file diff --git a/panel/app/views/users/index.php b/panel/app/views/users/index.php new file mode 100644 index 0000000..3072f7a --- /dev/null +++ b/panel/app/views/users/index.php @@ -0,0 +1,60 @@ +
    + +

    + + + ( pagination()->items() ?> ) + + + + + + + + +

    + +
    + +
    + + isCurrent()): ?> + + +
    + +
    + + + +
    \ No newline at end of file diff --git a/panel/app/widgets/account/account.html.php b/panel/app/widgets/account/account.html.php new file mode 100644 index 0000000..ffc5ef7 --- /dev/null +++ b/panel/app/widgets/account/account.html.php @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/panel/app/widgets/account/account.php b/panel/app/widgets/account/account.php new file mode 100644 index 0000000..d369ed8 --- /dev/null +++ b/panel/app/widgets/account/account.php @@ -0,0 +1,22 @@ +user(); + +return array( + 'title' => array( + 'text' => l('dashboard.index.account.title'), + 'link' => $user->url('edit'), + ), + 'options' => array( + array( + 'text' => l('dashboard.index.account.edit'), + 'icon' => 'pencil', + 'link' => $user->url('edit') + ) + ), + 'html' => function() use($user) { + return tpl::load(__DIR__ . DS . 'account.html.php', array( + 'user' => $user + )); + } +); \ No newline at end of file diff --git a/panel/app/widgets/history/history.html.php b/panel/app/widgets/history/history.html.php new file mode 100644 index 0000000..4d28c24 --- /dev/null +++ b/panel/app/widgets/history/history.html.php @@ -0,0 +1,18 @@ + diff --git a/panel/app/widgets/history/history.php b/panel/app/widgets/history/history.php new file mode 100644 index 0000000..fd50020 --- /dev/null +++ b/panel/app/widgets/history/history.php @@ -0,0 +1,13 @@ + array( + 'text' => l('dashboard.index.history.title'), + 'link' => false, + ), + 'html' => function() { + return tpl::load(__DIR__ . DS . 'history.html.php', array( + 'history' => panel()->user()->history()->get() + )); + } +); \ No newline at end of file diff --git a/panel/app/widgets/license/license.html.php b/panel/app/widgets/license/license.html.php new file mode 100644 index 0000000..ae0bb43 --- /dev/null +++ b/panel/app/widgets/license/license.html.php @@ -0,0 +1,15 @@ + +
    +
    + +
    +
    \ No newline at end of file diff --git a/panel/app/widgets/license/license.php b/panel/app/widgets/license/license.php new file mode 100644 index 0000000..006904a --- /dev/null +++ b/panel/app/widgets/license/license.php @@ -0,0 +1,25 @@ +license(); + +if($license->type() == 'trial' and !$license->local()) { + + return array( + 'title' => array( + 'text' => l('dashboard.index.license.title'), + 'link' => false, + 'compressed' => false + ), + 'html' => function() { + return tpl::load(__DIR__ . DS . 'license.html.php', array( + 'text' => kirbytext(str::template(l('dashboard.index.license.text'), array( + 'buy' => 'http://getkirby.com/buy', + 'docs' => 'http://getkirby.com/docs/installation/license-code' + ))) + )); + } + ); + +} else { + return false; +} \ No newline at end of file diff --git a/panel/app/widgets/pages/pages.html.php b/panel/app/widgets/pages/pages.html.php new file mode 100644 index 0000000..785f4a3 --- /dev/null +++ b/panel/app/widgets/pages/pages.html.php @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/panel/app/widgets/pages/pages.php b/panel/app/widgets/pages/pages.php new file mode 100644 index 0000000..a2ffd01 --- /dev/null +++ b/panel/app/widgets/pages/pages.php @@ -0,0 +1,36 @@ +site(); +$options = array(); + +if($site->canHaveSubpages()) { + $options[] = array( + 'text' => l('dashboard.index.pages.edit'), + 'icon' => 'pencil', + 'link' => $site->url('subpages') + ); +} + +if($addbutton = $site->addButton()) { + $options[] = array( + 'text' => l('dashboard.index.pages.add'), + 'icon' => 'plus-circle', + 'link' => $addbutton->url(), + 'modal' => $addbutton->modal(), + 'key' => '+', + ); +} + +return array( + 'title' => array( + 'text' => l('dashboard.index.pages.title'), + 'link' => $site->url('subpages'), + 'compressed' => true + ), + 'options' => $options, + 'html' => function() use($site) { + return tpl::load(__DIR__ . DS . 'pages.html.php', array( + 'pages' => $site->children()->paginated('sidebar') + )); + } +); \ No newline at end of file diff --git a/panel/app/widgets/site/site.html.php b/panel/app/widgets/site/site.html.php new file mode 100644 index 0000000..5f4c1d9 --- /dev/null +++ b/panel/app/widgets/site/site.html.php @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/panel/app/widgets/site/site.php b/panel/app/widgets/site/site.php new file mode 100644 index 0000000..304f28b --- /dev/null +++ b/panel/app/widgets/site/site.php @@ -0,0 +1,12 @@ + array( + 'text' => l('dashboard.index.site.title'), + 'link' => url(), + 'target' => '_blank', + ), + 'html' => function() { + return tpl::load(__DIR__ . DS . 'site.html.php'); + } +); \ No newline at end of file diff --git a/panel/assets/css/form.min.css b/panel/assets/css/form.min.css new file mode 100644 index 0000000..e3daf73 --- /dev/null +++ b/panel/assets/css/form.min.css @@ -0,0 +1 @@ +.field-with-headline:first-child{padding-top:0}.field-with-headline{counter-increment:count;padding-top:6em}.field-with-headline .hgroup span{padding-left:1.5em}.field-with-headline .hgroup:before{position:absolute;content:counter(count,decimal-leading-zero);left:0;color:#8dae28;font-weight:400}.field-with-image select{margin-left:3rem}.field-with-image .input-preview{position:absolute;top:2px;left:2px;bottom:2px;width:2.75em;background:url(../images/pattern.png)}.field-with-image .input-preview figure{display:block;width:100%;height:100%;background-repeat:no-repeat;background-position:center center;background-size:cover}.structure{padding-bottom:.5em}.structure-entry{background:#fff;border:2px solid #ddd;margin-bottom:.5em}.structure-readonly .structure-entry{background:#efefef;color:#777}.structure-entry:last-child{margin-bottom:0}.structure-entry-content{padding:1em 1.5em;border-bottom:1px solid #efefef}.structure[data-sortable=true] .structure-entry-content{cursor:move}.structure-entry-options .btn{padding:.75em 1.5em;width:50%;float:left;border-right:1px solid #efefef}.structure-empty{padding:1.5em;background:#ddd}.fileview-sidebar .structure-empty{background:0 0;border-radius:5px;border:1px dashed #ddd;padding:1rem 1.5rem 1.25rem}.structure-empty a{border-bottom:2px solid #aaa;margin-left:.5em}.fileview-sidebar .structure-empty a{display:inline-block;margin-left:0}.structure-empty a:hover{border-color:#000}.structure-add-button{cursor:pointer}.structure-table{width:100%;border-spacing:0;border:2px solid #ddd;border-bottom:1px solid #ddd;border-right:1px solid #ddd;table-layout:fixed}.structure-table td,.structure-table th{background:#fff;border-bottom:1px solid #ddd;border-right:1px solid #ddd;text-align:left;vertical-align:top}.structure-table th{padding:.5em;font-weight:400;color:#777;font-style:italic}.structure-table td a{display:block;padding:.5em;overflow:hidden;width:100%;text-overflow:ellipsis;cursor:move}.structure-table-options{width:3rem;text-align:center}.structure-table .structure-table-options a{text-align:center;cursor:pointer}.structure-sortable-helper{border-top:1px solid #ddd;border-left:1px solid #ddd}.field-counter{position:absolute;z-index:-1;right:0;top:0;text-align:right;font-size:.9em;line-height:1.66666666666667}.field-counter.outside-range{color:#b3000a} \ No newline at end of file diff --git a/panel/assets/css/panel.min.css b/panel/assets/css/panel.min.css new file mode 100644 index 0000000..b65c47f --- /dev/null +++ b/panel/assets/css/panel.min.css @@ -0,0 +1,4 @@ +@charset "UTF-8";body.ltr .shiv-left:after,body.rtl .shiv-right:after{left:-2em}#nprogress,.field-icon{pointer-events:none}.dashboard-items,.input-list-item,.nav>li{list-style:none}.btn-with-icon,.cut,.dashboard-item,.draggable-helper,.dropdown-list>li,.field-buttons,.tag .tag-label{white-space:nowrap}.breadcrumb-label:after,.breadcrumb-link:after,.breadcrumb-link:before,.breadcrumb-list:after,.cf:after,.field-buttons:after,.field-buttons:before,.hgroup a:after,.languages-toggle span:after,.shiv:after,.sidebar-toggle:after,.topbar .message-content:after,body>.message .message-content:after,form.loading:after{content:""}.cut,.dashboard-item-text,.dropdown-list>li>a,.input-list-item .input,.uid-preview{text-overflow:ellipsis}@font-face{font-family:'Source Sans Pro';font-style:normal;font-weight:400;src:url(../fonts/sourcesanspro-400.woff2) format("woff2"),url(../fonts/sourcesanspro-400.woff) format("woff")}@font-face{font-family:'Source Sans Pro';font-style:normal;font-weight:600;src:url(../fonts/sourcesanspro-600.woff2) format("woff2"),url(../fonts/sourcesanspro-600.woff) format("woff")}@font-face{font-family:'Source Sans Pro';font-style:italic;font-weight:400;src:url(../fonts/sourcesanspro-400-italic.woff2) format("woff2"),url(../fonts/sourcesanspro-400-italic.woff) format("woff")}*,:after,:before{margin:0;padding:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.text figure,.text p{margin-bottom:1.5em}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}abbr{border:none}img{max-width:100%}img.lazy{opacity:0;-webkit-transition:opacity .3s;-moz-transition:opacity .3s;-ms-transition:opacity .3s;transition:opacity .3s}img.lazy.has-loaded{opacity:1}body,html{height:100%}body.rtl{direction:rtl}.cf:after{display:table;clear:both}.cut{overflow-x:hidden}.hidden{display:none!important}.nav>li>a,.text figure,.uid-preview,hr{display:block}.grey{background:#efefef}.white{background:#fff}.app{padding-top:3em;background:#efefef}.section{padding:1.5em}.bars{position:relative;min-height:100%}.draggable-helper{position:fixed;z-index:10000;background:#000;padding:.25em 1em;color:#fff;width:auto!important;list-style:none;border-radius:3px;cursor:pointer}.draggable-helper a{color:#fff!important}.draggable-helper-with-image{border:2px solid #000;padding:0;line-height:0;border-radius:0;width:79px;height:79px;background:url(../images/pattern.png) #000}.draggable-helper-with-image img{position:relative;z-index:1;width:75px;height:75px;object-fit:cover}.uid-preview{max-width:20em;overflow:hidden;-ms-word-break:break-word;word-break:break-word}.shiv:after{position:absolute;width:2em;height:100%;top:0}body.ltr .shiv-right:after,body.rtl .shiv-left:after{right:-2em}.shiv-white{background:#fff}body.ltr .shiv-white:after{background:-webkit-linear-gradient(left,rgba(255,255,255,0),#fff);background:-moz-linear-gradient(left,rgba(255,255,255,0),#fff);background:-ms-linear-gradient(left,rgba(255,255,255,0),#fff);background:linear-gradient(left,rgba(255,255,255,0),#fff)}body.rtl .shiv-white:after{background:-webkit-linear-gradient(right,rgba(255,255,255,0),#fff);background:-moz-linear-gradient(right,rgba(255,255,255,0),#fff);background:-ms-linear-gradient(right,rgba(255,255,255,0),#fff);background:linear-gradient(right,rgba(255,255,255,0),#fff)}.shiv-grey{background:#efefef}body.ltr .shiv-grey:after{background:-webkit-linear-gradient(left,rgba(239,239,239,0),#efefef);background:-moz-linear-gradient(left,rgba(239,239,239,0),#efefef);background:-ms-linear-gradient(left,rgba(239,239,239,0),#efefef);background:linear-gradient(left,rgba(239,239,239,0),#efefef)}body.rtl .shiv-grey:after{background:-webkit-linear-gradient(right,rgba(239,239,239,0),#efefef);background:-moz-linear-gradient(right,rgba(239,239,239,0),#efefef);background:-ms-linear-gradient(right,rgba(239,239,239,0),#efefef);background:linear-gradient(right,rgba(239,239,239,0),#efefef)}#nprogress .bar{background:#fff;position:fixed;z-index:1031;top:0;left:0;width:100%;height:2px}.alpha,.beta,.delta,.epsilon,.gamma,.text h1,.text h2,.text h3,.text h4,.text h5,.text h6,.zeta,h1,h2,h3,h4,h5,h6{font-size:1em;font-weight:600}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{color:inherit}*{font-size:100%;font-family:"Source Sans Pro","Apple LiGothic Medium","Microsoft JhengHei UI","Helvetica Neue",Arial,sans-serif}.label,.text{font-size:1em}.light{font-weight:300}.strong,b,strong{font-weight:600}a{text-decoration:none;color:#000}.marginalia{color:#777}hr{height:2px;background:#ddd;border:0}.text{line-height:1.5em}.text a{font-weight:400;border-bottom:2px solid #ddd}.text a:hover{border-color:#000}.text mark{background:#8dae28;padding:0 5px;color:#fff}body.ltr .text blockquote{padding-left:1.5em;border-left:6px solid #ddd}body.rtl .text blockquote{padding-right:1.5em;border-right:6px solid #ddd}body.ltr .nav-icon-left,body.rtl .nav-icon-right{border-right:1px solid #555}.text hr{margin:1.5em 0}.text ul{margin-bottom:1.5em}.text ul ol,.text ul ul{margin-bottom:0}body.ltr .text ul{margin-left:1em}body.rtl .text ul{margin-right:1em}.text ol{margin-bottom:1.5em}.text ol ol,.text ol ul{margin-bottom:0}body.ltr .text ol{margin-left:1.25em}body.rtl .text ol{margin-right:1.25em}.btn,.nav-icon{display:inline-block;line-height:1em}body.ltr .nav-bar>li{float:left}body.rtl .nav-bar>li{float:right}.nav-icon{background:#000;color:#fff;padding:1em 0;text-align:center;height:3em;width:4em}.nav-icon:hover{color:#999}body.ltr .nav-icon-right,body.rtl .nav-icon-left{border-left:1px solid #555}.btn{background:0 0;border:0;cursor:pointer;outline:0;vertical-align:middle;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;appearance:none}.dashboard-section,.field-grid-item,.tag,.tag-input{vertical-align:top}.input,.input-with-selectbox .selectbox{-webkit-appearance:none;-moz-appearance:none}.btn::-moz-focus-inner{border:0;padding:0}.btn-rounded{font-weight:600;border-radius:5em;border:2px solid #000;padding:.4em 1.5em}.btn-rounded:focus,.btn-rounded:hover{background:#000;color:#fff}.btn-negative{border-color:#b3000a;color:#b3000a}.btn-negative:focus,.btn-negative:hover{background:#b3000a}.btn-positive{border-color:#8dae28;color:#8dae28}.btn-positive:focus,.btn-positive:hover{background:#8dae28}.btn-with-icon{color:#777}body.ltr .btn-with-icon{text-align:left}body.rtl .btn-with-icon{text-align:right}.buttons-centered,.dropload-text,.dropzone-text,.field-buttons,.field-buttons .btn,.field-icon .icon,.field-icon span,.message-toggle i{text-align:center}.btn-with-icon .icon,.btn-with-icon:focus,.btn-with-icon:hover{color:#000}.btn-addit i{line-height:0}.btn-wide{min-width:10em}fieldset{border:0}form.loading:after{position:fixed;top:0;left:0;right:0;bottom:0;cursor:wait;z-index:10000}.label{position:relative;font-weight:600;display:block;line-height:1.5em;padding-bottom:.5em}.label abbr{color:#8dae28;border:0}body.ltr .label abbr{padding-left:.25em}body.rtl .label abbr{padding-right:.25em}.label-option{position:absolute;top:0;font-weight:400;color:#777;line-height:1.5em}.field,.field-content{position:relative}body.ltr .label-option{right:0}body.rtl .label-option{left:0}.label-option .icon,.label-option:hover{color:#000}.field{margin-bottom:1.5em}.field-help{display:block;font-size:.9em;padding:.5em 0;font-style:italic}.field-help .pw-suggestion{background:#dedede;padding:.15em .75em;border-radius:1em;line-height:1em;font-size:1em;font-family:Courier,monospace;font-style:normal;cursor:pointer}.field-help .pw-suggestion:hover{background:#8DAE28;color:#fff}.field-with-error .label,.field-with-error .label abbr{color:#b3000a}.field-help .pw-reload{position:relative;top:3px;border:0;margin-left:.5em}.field-with-error .input:focus{border-color:#000}.field-with-info{margin-bottom:0}.field-with-line{clear:both;margin:1.5em 0 2.5em}@media screen and (min-width:60em){.field-with-line{margin:3.5em 0 4.5em}}.field-icon{position:absolute;top:2px;bottom:2px;width:3em;background:#fff}body.ltr .field-icon{right:2px;border-left:1px dashed #ddd}body.rtl .field-icon{left:2px;border-right:1px dashed #ddd}.field-icon .icon{position:absolute;top:50%;left:0;right:0;margin-top:-7px;color:#777}.field-icon:hover .icon{color:#8dae28}body.ltr .field-with-icon .input{padding-right:3.5em}body.rtl .field-with-icon .input{padding-left:3.5em}.field-icon span{display:block;padding:0;line-height:3em;font-size:.8em;color:#777}.field-is-readonly .field-icon{background:#efefef}body.ltr .field-is-readonly .field-icon{border-left-color:#ccc}body.rtl .field-is-readonly .field-icon{border-right-color:#ccc}.field-is-disabled{opacity:.5}.field-is-disabled .label{color:#555}.field-buttons{position:absolute;bottom:2px;left:2px;right:2px;border-top:1px solid #efefef;background:#fff;line-height:0;overflow-y:hidden}.field-buttons:after,.field-buttons:before{position:absolute;top:0;bottom:0;width:1em;pointer-events:0;z-index:1}.breadcrumb-list:after,.file-preview object,.fileview-preview-link object,.input-is-readonly .selectbox-wrapper,.input-is-readonly[type=checkbox],.input-is-readonly[type=radio],.is-disabled .pika-button,body.over *{pointer-events:none}.field-buttons:before{left:0;background:-webkit-linear-gradient(left,rgba(0,0,0,.05),rgba(255,255,255,0));background:-moz-linear-gradient(left,rgba(0,0,0,.05),rgba(255,255,255,0));background:-ms-linear-gradient(left,rgba(0,0,0,.05),rgba(255,255,255,0));background:linear-gradient(left,rgba(0,0,0,.05),rgba(255,255,255,0))}.field-buttons:after{right:0;background:-webkit-linear-gradient(right,rgba(0,0,0,.05),rgba(255,255,255,0));background:-moz-linear-gradient(right,rgba(0,0,0,.05),rgba(255,255,255,0));background:-ms-linear-gradient(right,rgba(0,0,0,.05),rgba(255,255,255,0));background:linear-gradient(right,rgba(0,0,0,.05),rgba(255,255,255,0))}@media screen and (min-width:30em){.field-buttons:after,.field-buttons:before{display:none}}.field-buttons ul{overflow:auto}.input-with-checkbox,.input-with-radio,.input-with-selectbox,.selectbox-wrapper,.topbar .message{overflow:hidden}.field-buttons li{display:inline-block;border-right:1px solid #efefef;float:none!important}.field-buttons li:last-child{border-right:0}.field-buttons .btn{padding:.5em 1.5em;display:block;line-height:1em;width:100%}.field-buttons .btn:hover{color:#000}.field-with-buttons .input{min-height:10em;padding-bottom:1.5em!important}body.ltr .field-grid{margin-left:-1.5em}body.rtl .field-grid{margin-right:-1.5em}.field-grid-item{display:inline-block;width:100%}body.ltr .field-grid-item{padding-left:1.5em}body.rtl .field-grid-item{padding-right:1.5em}@media screen and (min-width:60em){.field-grid-item-1-2,.field-grid-item-2-4{width:50%}.field-grid-item-1-4{width:25%}.field-grid-item-3-4{width:75%}.field-grid-item-1-3{width:33.3333333%}.field-grid-item-2-3{width:66.666666%}.field-grid-item-1-5{width:20%}.field-grid-item-2-5{width:40%}.field-grid-item-3-5{width:60%}.field-grid-item-4-5{width:80%}}.input,.range,.selectbox{width:100%}.input{padding:.5em;font-size:1em;line-height:1.5em;font-weight:400;border:2px solid #ddd;background:#fff;display:block;-ms-appearance:none;appearance:none;border-radius:0;min-height:2.75em}.input:-webkit-autofill{box-shadow:0 0 0 1000px #fff inset!important}.input:focus{outline:0;border-color:#8dae28}.input.over{border-color:#000}textarea.input{resize:none}.input:invalid,.input:required{box-shadow:none}.input-is-readonly{background:#efefef;border-color:#ddd;color:#777}.input-is-readonly.input-is-focused,.input-is-readonly:focus{border-color:#ddd}.input-is-focused{border-color:#8dae28}.input-with-radio label{cursor:pointer}.input-with-radio .radio{position:relative;top:-1px}body.ltr .input-with-radio .radio{margin:0 1em 0 .25em}body.rtl .input-with-radio .radio{margin:0 .25em 0 1em}body.ltr .input-with-checkbox .checkbox{float:left;margin:.3em 1em .3em .25em}body.rtl .input-with-checkbox .checkbox{float:right;margin:.3em .25em .3em 1}.selectbox-wrapper{height:1.5em}.input-with-selectbox{cursor:pointer;padding:.5em;line-height:1em}body.ltr .input-with-selectbox .selectbox-wrapper{margin-right:-3em}body.rtl .input-with-selectbox .selectbox-wrapper{margin-left:-3em}.input-with-selectbox .selectbox{line-height:1.5em;display:block;cursor:pointer;appearance:none;border:0;background:0 0;outline:0;border-radius:0}.input-with-selectbox .selectbox:-moz-focusring{color:transparent;text-shadow:0 0 0 #000}.input-with-fileupload{line-height:1em;padding:.75em .5em}.input-with-tags{padding:3px}.input-with-tags.input-is-readonly .tag{opacity:.25}body.ltr .field-with-icon .input-with-tags{padding-right:3.25em}body.rtl .field-with-icon .input-with-tags{padding-left:3.25em}.tag-input{margin:3px;display:block;-moz-appearance:none;-webkit-appearance:none;-ms-appearance:none;appearance:none;border:0;padding:.25em;width:auto;outline:0;background:0 0}@media screen and (min-width:30em){.tag-input{display:inline-block}}.input-list-item{margin-bottom:.5em}.input-list-item .input{white-space:nowrap;overflow:hidden}.input-with-items{padding:0}.input-with-items .item{border-bottom:1px solid #efefef}.input-with-items .item:last-child{border-bottom:0}.buttons{margin:.5em 0}body.ltr .buttons .btn-cancel{float:left}body.rtl .buttons .btn-cancel{float:right}body.ltr .buttons .btn-submit{float:right;margin-left:1rem}body.rtl .buttons .btn-submit{float:left;margin-right:1rem}.btn-submit .btn:first-child{border-top-right-radius:0;border-bottom-right-radius:0;padding-right:1rem}.btn-submit .btn:last-child{border-top-left-radius:0;border-bottom-left-radius:0;border-left:0;padding-left:1rem}.buttons-centered .btn{margin:0 .5em}.buttons-centered .btn-cancel,.buttons-centered .btn-submit{float:none!important}#form-field-username{text-transform:lowercase}@media screen and (min-width:50em){.mainbar .form{padding:0 1.5em 9em}.mainbar .form .buttons{position:fixed;bottom:0;background:#efefef;background:-webkit-linear-gradient(bottom,#efefef,rgba(239,239,239,.9) 75%,rgba(239,239,239,0));background:-moz-linear-gradient(bottom,#efefef,rgba(239,239,239,.9) 75%,rgba(239,239,239,0));background:-ms-linear-gradient(bottom,#efefef,rgba(239,239,239,.9) 75%,rgba(239,239,239,0));background:linear-gradient(bottom,#efefef,rgba(239,239,239,.9) 75%,rgba(239,239,239,0));margin:0;padding:1.5em 3em}.ltr .mainbar .form .buttons{left:33.33%;right:0}.rtl .mainbar .form .buttons{right:33.33%;left:0}.mainbar .form .buttons .text span{position:relative;padding:.35rem 2rem;background:#8dae28;display:inline-block;margin:0 auto 1.5rem;color:#fff;font-weight:600;box-shadow:rgba(0,0,0,.05) 0 2px 10px}.mainbar .form .buttons .text span:after{position:absolute;content:"";border-top:5px solid #8dae28;border-left:5px solid transparent;border-right:5px solid transparent;bottom:-5px;left:50%;margin-left:-5px}}.dropdown,.modal-content{box-shadow:rgba(0,0,0,.2) 0 2px 10px}.gu-mirror{position:fixed!important;margin:0!important;z-index:9999!important}.gu-hide{display:none!important}.gu-unselectable{-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important;user-select:none!important}.gu-transit{opacity:0;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";filter:alpha(opacity=0)}.dropzone{position:relative;border:1px dashed #ddd;border-radius:.2em}.grey .dropzone{border-color:#bbb}.dropzone-text{line-height:1.5em}.dropzone-text .marginalia{font-size:.9em;font-style:italic}.icon,.message-toggle i,.tag .tag-x{font-style:normal}.dropzone-progress{position:absolute;top:50%;left:0;right:0;margin-top:-5px;height:10px;border-radius:2em;background:#ddd;display:none}.dropzone-progress span{display:block;border-radius:2em;width:0;height:100%;background:#8dae28}.dropzone-is-loading{border-color:transparent!important}.dropzone-is-loading .dropzone-progress{display:block}.dropzone-is-loading .dropzone-text{opacity:.05}.dropzone.dropzone-input{border-width:2px;transition:border-color .3s;cursor:pointer}.dropzone.dropzone-input:hover{border-color:#bbb}.dropzone.dropzone-input:focus{border-color:#8dae28;outline:0}.grey .dropzone.dropzone-input:hover{border-color:#aaa}.dropload{position:relative;border:2px dashed rgba(0,0,0,.15);border-radius:.2em;transition:border-color .3s;margin-bottom:1.5em}.dropload.over,.dropload:focus,.dropload:hover{border-color:#8dae28}.dropload-text{display:none}.dropload-text strong{display:block}.dropload.is-active [type=file]{position:absolute;top:0;left:0;right:0;bottom:0;width:100%;opacity:0;cursor:pointer}.dropload.is-active [type=submit]{display:none}.dropload.is-active .dropload-text,.tag{display:block}.message{position:relative;color:#fff;line-height:1em;cursor:pointer}.message-is-notice .message-content{background:#8dae28}.message-is-alert .message-content{background:#b3000a}.message-is-alert a{color:#fff;border-bottom:2px solid rgba(255,255,255,.5)}.message-content{display:block;line-height:1.5em;background:#000}body.ltr .message-content{padding:.75em 4.5em .75em 1.5em}body.rtl .message-content{padding:.75em 1.5em .75em 4.5em}.message-toggle{position:absolute;top:50%;margin-top:-.75em;border:2px solid rgba(255,255,255,.5);border-radius:4em;width:1.5em;height:1.5em;transition:border-color .3s;cursor:pointer}body.ltr .message-toggle{right:1.5em}body.rtl .message-toggle{left:1.5em}.message-toggle:hover{border-color:#fff}.message-toggle i{position:absolute;top:50%;left:50%;font-size:1.25em;-webkit-transform:translate(-50%,-50%);-moz-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.message-is-notice .message-toggle{color:#fff}body>.message{position:absolute;top:0;height:3em;z-index:1;background:#000}body.ltr body>.message{right:0;left:4em;margin-left:-1px}body.rtl body>.message{left:0;right:4em;margin-right:-1px}body>.message .message-content{position:relative}body.ltr body>.message .message-content{margin-right:4.5em;padding-right:1.5em}body.rtl body>.message .message-content{margin-left:4.5em;padding-left:1.5em}body>.message .message-content:after{position:absolute;top:50%;margin-top:-.5em;border-top:.5em solid transparent;border-bottom:.5em solid transparent}body.ltr body>.message .message-content:after{right:-.5em;border-left:.5em solid #8dae28}body.rtl body>.message .message-content:after{left:-.5em;border-right:.5em solid #8dae28}body.ltr body>.message-is-alert .message-content:after{border-left:.5em solid #b3000a}body.rtl body>.message-is-alert .message-content:after{border-right:.5em solid #b3000a}.modal-content .message{margin:-1.5em -1.5em 1.5em}@keyframes showTopbarMessage{0%{-webkit-transform:translateX(6rem);-moz-transform:translateX(6rem);-ms-transform:translateX(6rem);transform:translateX(6rem);opacity:0}100%{-webkit-transform:translateX(0);-moz-transform:translateX(0);-ms-transform:translateX(0);transform:translateX(0);opacity:1}}.file-preview img,.file-preview object,.fileview-image-link{-webkit-transform:translate(-50%,-50%);-moz-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}.topbar .message{position:absolute;top:0;z-index:100;background:#000}.ltr .topbar .message{right:0}.rtl .topbar .message{left:0}.topbar .message-content:after{position:absolute;top:50%;margin-top:-7px;border-top:7px solid transparent;border-bottom:5px solid transparent}body.ltr .topbar .message-content:after{right:-7px;border-left:7px solid #8dae28}body.rtl .topbar .message-content:after{left:-7px;border-right:7px solid #8dae28}body.ltr .topbar .message-is-alert .message-content:after{border-left:7px solid #b3000a}body.rtl .topbar .message-is-alert .message-content:after{border-right:7px solid #b3000a}body.ltr .topbar .message-toggle{right:1.25em}.modal,body.rtl .dropdown-left{right:0}body.rtl .topbar .message-toggle{left:1.25em}.modal,body.ltr .dropdown-left{left:0}.topbar .message-content{position:relative;animation-name:showTopbarMessage;animation-iteration-count:1;animation-timing-function:ease-out;animation-duration:.3s;min-width:4.5em;text-align:center}body.ltr .topbar .message-content{margin-right:4rem;padding-right:1.5rem}body.rtl .topbar .message-content{margin-left:4rem;padding-left:1.5em}.tag{margin:3px;position:relative;cursor:pointer;background:#000;border-radius:3px}a.tag{line-height:1.25em}@media screen and (min-width:30em){.tag{display:inline-block}}.tag .tag-label{display:block;width:100%;color:#fff;z-index:1;padding:.25em 1em;text-align:left;border-radius:3px}.tag .tag-label:focus,.tag-is-focused .tag-label{outline:0;background:#8dae28}.tag button{border:0;background:0 0;cursor:pointer}.tag button::-moz-focus-inner{border:0;padding:0}body.ltr .tag button.tag-label{padding-right:3.5em}body.rtl .tag button.tag-label{padding-left:3.5em}.tag .tag-x{position:absolute;top:0;line-height:1em;padding:.4em 1em;z-index:1;color:rgba(255,255,255,.7)}body.ltr .tag .tag-x{right:0;border-left:1px solid rgba(255,255,255,.3)}body.rtl .tag .tag-x{left:0;border-right:1px solid rgba(255,255,255,.3)}.tag .tag-x:focus,.tag .tag-x:hover{color:#fff}.modal{position:fixed;top:0;bottom:0;height:100%;background:rgba(30,30,30,.8);z-index:1000;overflow-y:scroll;-webkit-overflow-scrolling:touch;padding-top:3rem}.modal-content{position:relative;background:#fff;overflow:auto;z-index:2;border-radius:3px;margin:0 1.5em 3em}.modal-content .form{padding:1.5em}@media screen and (min-width:30em){.modal-content{width:22em;margin-left:auto;margin-right:auto}.modal-content-large{width:90%;max-width:40em}.modal-content-medium{width:70%;max-width:30em}}.instruction{text-align:center;padding:6em 0}.instruction-content{display:inline-block;padding:0 1.5em 1.5em;text-align:center}.instruction-text{line-height:1.5em;margin-bottom:1.5em}.dashboard-section{position:relative;display:inline-block;margin-bottom:1.5em;width:100%;padding:1em 1.5em}.dashboard-section ul,.dashboard-section:last-child{margin-bottom:0}.dashboard-box,.dashboard-section .field:last-child{margin-bottom:.5em}.dashboard-box{position:relative;display:block;border:2px solid #ddd;overflow:hidden;background:#efefef}.dashboard-item-icon,.dashboard-item-text{display:inline-block;vertical-align:middle}.dashboard-box .text{padding:.75em 1em}.dashboard-box .text :last-child{margin-bottom:0}.dashboard-items .dashboard-item{border-bottom:1px solid #ddd}.dashboard-items .dashboard-item:last-child{border-bottom:0}.dashboard-item-icon{height:2.5em;width:2.5em;text-align:center}.dashboard-item-icon-with-border{border-right:1px solid #ddd}.dashboard-item-icon i{position:static;padding-top:.8em}.dashboard-item-text{overflow:hidden;padding:0 .75em;width:calc(100% - 2.5em)}.dropdown{position:relative;background:#fff;display:none}.dropdown:after{position:absolute;content:"";top:-.5em;border-bottom:.5em solid #fff;border-left:.5em solid transparent;border-right:.5em solid transparent}.hgroup,.hgroup a,.icon{position:relative}body.ltr .dropdown:after{margin-left:-.1em}body.rtl .dropdown:after{margin-right:-.1em}body.ltr .dropdown-left:after{left:1.5em}body.rtl .dropdown-left:after{right:1.5em}body.ltr .dropdown-right{right:0}body.rtl .dropdown-right{left:0}body.ltr .dropdown-right:after{right:1.5em}body.rtl .dropdown-right:after{left:1.5em}body.ltr .hgroup a:after,body.rtl .hgroup-single-line .hgroup-options{left:0}.dropdown-list>li>a{padding:.75em 1.5em .75em 1em;overflow:hidden;border-bottom:1px solid #efefef}body.ltr .icon-left,body.rtl .icon-right{padding-right:.5em}.dropdown-list>li>a:hover{color:#777}.dropdown-list>li>a:hover i{color:#000}.dropdown-list>li i{display:inline-block;width:2em;text-align:center}.dropdown-dark{background:#000;border-radius:4px}.dropdown-dark:after{display:none}.dropdown-dark .dropdown-list a{color:#fff;border-bottom:1px solid #222}.dropdown-dark .dropdown-list a:hover,.dropdown-dark .dropdown-list a:hover i{color:#999}.dropdown-dark .dropdown-list li:last-child a{border-bottom:0}.icon{font-size:14px;top:-1px;display:inline-block;font-weight:700;text-align:center}body.ltr .icon-right,body.rtl .icon-left{padding-left:.5em}.avatar{display:block;line-height:0}.avatar img{width:4.5em;height:4.5em;padding:2px;border:2px solid #ddd}.hgroup,.hgroup-title{border-bottom:2px solid #ddd;padding-bottom:.5em}.avatar-large img{width:9em;height:9em}.avatar-centered{text-align:center}.avatar-full img{width:100%;height:auto;max-width:100%}.hgroup{line-height:1.5em;margin-bottom:1.5em}.hgroup a{display:inline-block}.hgroup a:after{position:absolute;bottom:-.5em;margin-bottom:-2px;height:2px;width:100%;background:#000}body.rtl .hgroup a:after{right:0}.hgroup-title{display:block;margin-bottom:.5em;font-weight:600;font-size:1em}.hgroup-options{display:block;font-size:1em;font-weight:400;color:#777}.hgroup-options a{display:inline-block}.hgroup-options a:hover{color:#000}body.ltr .hgroup-option-left{float:left}body.ltr .hgroup-option-right,body.rtl .hgroup-option-left{float:right}body.rtl .hgroup-option-right{float:left}body.ltr .hgroup-option-right a{margin-left:1em}body.rtl .hgroup-option-right a{margin-right:1em}@media screen and (max-width:70em){body.ltr .hgroup-option-right>a .icon-left{padding-right:0}body.rtl .hgroup-option-right>a .icon-left{padding-left:0}.hgroup-option-right>a span{display:none}}.file,.file-meta,.file-name,.file-preview{display:block}.hgroup-options .icon{top:0;color:#000}.hgroup-single-line{border:0;padding-bottom:0}.hgroup-single-line .hgroup-title{margin-bottom:0}.hgroup-single-line .hgroup-options{position:absolute;top:0}body.ltr .hgroup-single-line .hgroup-options{right:0}.hgroup-compressed{margin-bottom:.5em}.hgroup .dropdown{position:absolute;z-index:1;margin-top:-2px}.hgroup .dropdown a{font-weight:400}.file{background:#fff}.file-preview{position:relative;padding-bottom:66.66%;line-height:0;background:#000;overflow:hidden}.file-preview-is-image{background:url(../images/pattern.png)}.item,.item-options{background:#fff}.file-preview img,.file-preview object{position:absolute;top:50%;left:50%;max-width:100%;max-height:100%;transform:translate(-50%,-50%)}.file-preview span{position:absolute;top:50%;left:0;right:0;text-align:center;text-transform:uppercase;color:#fff}.item,.item-content{position:relative;overflow:hidden}.file-info{padding:1em 1.5em;line-height:1.5em;border-bottom:1px solid #ddd}.file-name{color:#000}.file-options .btn{width:50%;border-bottom:0;padding:.75em 1.5em}body.ltr .file-options .btn{float:left;text-align:center}body.rtl .file-options .btn{float:right;text-align:center}.file-options .btn:first-child{border-right:1px solid #ddd}.file-options .btn span{display:none}.file-options .btn:last-child{border-bottom:0}@media screen and (min-width:120em){.file-options .btn{padding:1em 1.5em}.file-options .btn span{display:inline}}.items .item,.items.users{margin-bottom:1.5em}.items-with-borders{border:1px solid #ddd}.items .item:last-child{margin-bottom:0}@media screen and (min-width:50em){.items .item{margin-bottom:1px}}.item{line-height:1em}.item-content{border-bottom:1px solid #efefef}.item-info{padding:1em;height:4.25em;overflow:hidden}.item-meta,.item-title{padding:0 .5em;display:block;white-space:nowrap}.item-title{margin-bottom:.25em}.item-options li{width:50%}body.ltr .item-options li{border-left:1px solid #efefef}body.rtl .item-options li{border-right:1px solid #efefef}.item-options-three li{width:33.33%}body.ltr .item-options li:first-child{border-left:0}body.rtl .item-options li:first-child{border-right:0}.item-options .btn{padding:1em 1.5em}@media screen and (min-width:50em){.item-content{border:0;line-height:1em}.item-info{height:3em}.item-title{margin-bottom:0}body.ltr .item-title{float:left}body.rtl .item-title{float:right}body.ltr .item-meta{float:left}body.rtl .item-meta{float:right}.item-options{position:absolute;top:0}body.ltr .item-options{right:0;border-left:1px solid #efefef}body.rtl .item-options{left:0;border-left:1px solid #efefef}.item-options li{width:auto}}.item-image{position:absolute;width:4.25em;height:4.25em}.item-image img{height:100%;width:100%}body.ltr .item-with-image .item-info{margin-left:4.25em}body.rtl .item-with-image .item-info{margin-right:4.25em}@media screen and (min-width:50em){.item-image{width:3em;height:3em}body.ltr .item-with-image .item-info{margin-left:3em}body.rtl .item-with-image .item-info{margin-right:3em}}.item-condensed .item-info{padding:.75em .5em;height:auto}.item-condensed .item-options .btn{padding:1em}@media screen and (min-width:50em){.item-condensed .item-info{height:auto}.item-condensed .item-options .btn{padding:.75em 1.25em}.item-condensed .item-options a span{display:none}body.ltr .item-condensed .item-options i{padding-right:0}body.rtl .item-condensed .item-options i{padding-left:0}}.item-condensed.item-with-image .item-image{position:absolute;width:3em;height:3em}body.ltr .item-condensed.item-with-image .item-info{margin-left:3.75em}body.rtl .item-condensed.item-with-image .item-info{margin-right:3.75em}@media screen and (min-width:50em){.item-condensed.item-with-image .item-image{width:2.5em;height:2.5em}body.ltr .item-condensed.item-with-image .item-info{margin-left:2.5em}body.rtl .item-condensed.item-with-image .item-info{margin-right:2.5em}}.item-options .icon.marginalia{min-width:1em}.breadcrumb{position:relative;background:#000}.breadcrumb-list{height:3em;overflow:hidden;display:none}.breadcrumb-list:after{position:absolute;top:0;bottom:0;width:2em}body.ltr .breadcrumb-list:after{right:0;background:-webkit-linear-gradient(left,transparent,#000);background:-moz-linear-gradient(left,transparent,#000);background:-ms-linear-gradient(left,transparent,#000);background:linear-gradient(left,transparent,#000)}body.rtl .breadcrumb-list:after{left:0;background:-webkit-linear-gradient(right,transparent,#000);background:-moz-linear-gradient(right,transparent,#000);background:-ms-linear-gradient(right,transparent,#000);background:linear-gradient(right,transparent,#000)}.breadcrumb-list li:last-child .breadcrumb-label{max-width:none}.breadcrumb-link{position:relative;line-height:1em;color:#fff;font-weight:400}body.ltr .breadcrumb-link{padding-left:1.25em}body.rtl .breadcrumb-link{padding-right:1.25em}.breadcrumb-link:focus{outline:0}.breadcrumb-label{display:block;white-space:nowrap;overflow:hidden;max-width:6em}body.ltr .breadcrumb-label{padding:1em .75em 1em 0}body.rtl .breadcrumb-label{padding:1em 0 1em .75em}@media screen and (min-width:50em){.breadcrumb .nav-icon,.breadcrumb-dropdown{display:none}.breadcrumb ul{display:block}}.breadcrumb-label:after{position:absolute;top:0;bottom:0;width:.75em}body.ltr .breadcrumb-label:after{right:0;background:-webkit-linear-gradient(left,transparent,#000);background:-moz-linear-gradient(left,transparent,#000);background:-ms-linear-gradient(left,transparent,#000);background:linear-gradient(left,transparent,#000)}body.rtl .breadcrumb-label:after{left:0;background:-webkit-linear-gradient(right,transparent,#000);background:-moz-linear-gradient(right,transparent,#000);background:-ms-linear-gradient(right,transparent,#000);background:linear-gradient(right,transparent,#000)}.breadcrumb-link:hover{color:#999}.breadcrumb-link:after,.breadcrumb-link:before{position:absolute;height:1.6em;width:1px;z-index:1}body.ltr .breadcrumb-link:after,body.ltr .breadcrumb-link:before{right:0}body.rtl .breadcrumb-link:after,body.rtl .breadcrumb-link:before{left:0}.breadcrumb-link:after{top:0;background:-webkit-linear-gradient(bottom,#555,#555 50%,#000);background:-moz-linear-gradient(bottom,#555,#555 50%,#000);background:-ms-linear-gradient(bottom,#555,#555 50%,#000);background:linear-gradient(bottom,#555,#555 50%,#000)}body.ltr .breadcrumb-link:after{-webkit-transform:rotate(-33.75deg);-moz-transform:rotate(-33.75deg);-ms-transform:rotate(-33.75deg);transform:rotate(-33.75deg)}body.rtl .breadcrumb-link:after{-webkit-transform:rotate(33.75deg);-moz-transform:rotate(33.75deg);-ms-transform:rotate(33.75deg);transform:rotate(33.75deg)}.breadcrumb-link:before{bottom:0;background:-webkit-linear-gradient(top,#555,#555 50%,#000);background:-moz-linear-gradient(top,#555,#555 50%,#000);background:-ms-linear-gradient(top,#555,#555 50%,#000);background:linear-gradient(top,#555,#555 50%,#000)}.languages,.topbar{z-index:100;background:#000}body.ltr .breadcrumb-link:before{-webkit-transform:rotate(33.75deg);-moz-transform:rotate(33.75deg);-ms-transform:rotate(33.75deg);transform:rotate(33.75deg)}body.rtl .breadcrumb-link:before{-webkit-transform:rotate(-33.75deg);-moz-transform:rotate(-33.75deg);-ms-transform:rotate(-33.75deg);transform:rotate(-33.75deg)}.languages{position:absolute;top:0;width:4.5em;bottom:0;text-align:center}.grid-item,.languages .dropdown{width:100%}body.ltr .languages{right:4em;border-left:1px solid #555}body.rtl .languages{left:4em;border-right:1px solid #555}.languages-toggle{display:block;color:#fff;line-height:3em;text-transform:uppercase}.languages-toggle span{position:relative;display:block}body.ltr .languages-toggle span{padding-right:1.25em}body.rtl .languages-toggle span{padding-left:1.25em}.languages-toggle span:after{position:absolute;top:50%;border-top:.3em solid #fff;border-left:.3em solid transparent;border-right:.3em solid transparent}body.ltr .languages-toggle span:after{right:1em}body.rtl .languages-toggle span:after{left:1em}body.ltr .languages .dropdown:after{left:50%;margin-left:-.5em}body.rtl .languages .dropdown:after{right:50%;margin-right:-.5em}.pagination{position:relative;margin-bottom:2.5em}.pagination a,.pagination span{padding:.5em 0;display:block}body.ltr .pagination .pagination-prev{float:left;padding-right:1em}body.ltr .pagination .pagination-next,body.rtl .pagination .pagination-prev{float:right;padding-left:1em}.pagination-index{position:absolute;left:2em;right:2em;text-align:center;color:#777}.topbar,body.ltr .topbar .nav-icon-right,body.rtl .topbar .nav-icon-left{right:0}.pagination-index select{position:absolute;top:0;bottom:0;left:50%;cursor:pointer;-webkit-transform:translate(-50%,0);-moz-transform:translate(-50%,0);-ms-transform:translate(-50%,0);transform:translate(-50%,0);min-width:6em;opacity:0}body.ltr .topbar .nav-icon-left,body.rtl .topbar .nav-icon-right{left:0}body.rtl .pagination .pagination-next{float:left;padding-right:1em}.pagination .pagination-inactive{color:#ddd;pointer-events:none}body.ltr .grid{margin-left:-1.5em}body.rtl .grid{margin-right:-1.5em}.grid-item{display:inline-block;vertical-align:top}body.ltr .grid-item{padding-left:1.5em}body.rtl .grid-item{padding-right:1.5em}body.ltr .grid-full{margin-left:0}body.rtl .grid-full{margin-right:0}body.ltr .grid-full .grid-item{padding-left:0}body.rtl .grid-full .grid-item{padding-right:0}.topbar{position:absolute;top:0;left:0;height:3em}.topbar .nav-icon{position:absolute;top:0}body.ltr .topbar .breadcrumb{margin-left:4em}body.rtl .topbar .breadcrumb{margin-right:4em}.topbar .dropdown{position:absolute;z-index:1000;top:3em}.sidebar{padding:1.5em}.sidebar-toggle{position:relative;font-weight:400;display:block;padding:1em 1.5em;color:#777;background:#efefef;border:2px solid #ddd;border-radius:3px}.sidebar-toggle:hover{color:#000}.sidebar-toggle:after{position:absolute;top:50%;margin-top:-.15em;border-top:.4em solid #000;border-left:.4em solid transparent;border-right:.4em solid transparent}body.ltr .sidebar-toggle:after{right:1.5em}body.rtl .sidebar-toggle:after{left:1.5em}.ltr .sidebar-list>li>.option,body.ltr .sidebar-list .icon{left:0}.sidebar-expanded .sidebar-toggle:before{content:attr(data-hide)}.main .loader:after,.subpages-help:after{content:""}.sidebar-expanded .sidebar-toggle{background:#fff;border-color:#fff;border-bottom:1px dashed #ddd}.sidebar-expanded .sidebar-toggle span{display:none}.sidebar-expanded .sidebar-toggle:after{border-top:0;border-bottom:.4em solid #000}.sidebar-content{display:none;background:#fff}.sidebar-expanded .sidebar-content{display:block}.sidebar-content>.marginalia{margin-bottom:1.5em;line-height:1.5em}@media screen and (min-width:50em){.mainbar,.sidebar{overflow-y:scroll;-webkit-overflow-scrolling:touch}.bars-with-sidebar-left:before,.sidebar{position:absolute;top:0;bottom:0;height:100%}body.ltr .bars-with-sidebar-left:before,body.ltr .sidebar,body.rtl .mainbar{left:0}body.ltr .mainbar,body.rtl .bars-with-sidebar-left:before,body.rtl .sidebar{right:0}.sidebar{width:33.33%;padding:0}.sidebar-toggle{display:none!important}.sidebar-content{display:block!important}.mainbar{position:absolute;top:0;bottom:0;width:66.66%}.bars-with-sidebar-left:before{content:"";width:33.33%;background:#fff;z-index:-1}}.sidebar-list{margin-bottom:1.5em}.sidebar-list>li{position:relative;white-space:nowrap;overflow:hidden}body.ltr .sidebar-list>li>a{padding:.25em 0 .25em 1.75em}body.rtl .sidebar-list>li>a{padding:.25em 1.75em .25em 0}.sidebar-list>li>.option{position:absolute;top:-1px;z-index:1;text-align:right;padding:0!important;background:#fff;display:none;font-size:18px}.rtl .sidebar-list>li>.option{right:0}.sidebar-list>li:hover .option{display:block}.sidebar-list>li:hover .draggable .icon{display:none}.sidebar-list .icon{position:absolute;top:.4em}body.rtl .sidebar-list .icon{right:0}.sidebar-list .marginalia{position:absolute}body.ltr .sidebar-list .marginalia{right:0;padding-left:.5em}body.rtl .sidebar-list .marginalia{left:0;padding-right:.5em}.sidebar-search{padding:.5em 0;margin:0 -.25em}.sidebar .pagination{border-top:2px solid #ddd;margin-top:-1em}.files .grid-item{margin-bottom:1.5em}@media screen and (min-width:40em){.dashboard{-webkit-column-count:2;-webkit-column-gap:1.5em;-moz-column-count:2;-moz-column-gap:1.5em;column-count:2;column-gap:1.5em}.files .grid-item{width:50%}}@media screen and (min-width:50em){.files .grid-item{width:33.33%}}@media screen and (min-width:60em){.dashboard{-webkit-column-count:3;-moz-column-count:3;column-count:3}.breadcrumb-label{max-width:9em}.files .grid-item{width:25%}}@media screen and (min-width:70em){.breadcrumb-label{max-width:12em}.files .grid-item{width:20%}}@media screen and (min-width:80em){.files .grid-item{width:16.66%}}.subpages{padding:.25em;margin-bottom:1em}body.ltr .subpages-help-left,body.rtl .subpages-help-right{padding-left:3em}body.ltr .subpages-help-right,body.rtl .subpages-help-left{padding-right:3em}.subpages .item{margin-bottom:.25em}.dropzone.subpages .items{min-height:2.5em}.subpages .items.sortable .item-info{cursor:move}.subpages-grid .grid-item{margin-bottom:1.5em}.subpages-grid h3{margin-bottom:1em;font-weight:400;color:#777}@media screen and (min-width:50em){.subpages .item{margin-bottom:1px}.subpages-grid .grid-item{width:50%}}.subpages-help{position:relative;font-style:italic}.subpages-help:after{position:absolute;top:-1.5em;height:6em;width:3em}.fileview-preview-link,.main{height:100%}body.ltr .subpages-help-right:after{right:0;background:url(../images/hint.arrows.png) top right no-repeat}body.ltr .subpages-help-left:after,body.rtl .subpages-help-right:after{left:0;background:url(../images/hint.arrows.png) top left no-repeat}body.rtl .subpages-help-left:after{right:0;background:url(../images/hint.arrows.png) top right no-repeat}.fileview{background:#fff}.fileview-image{position:relative;padding-bottom:66.66%;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyFpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDE0IDc5LjE1MTQ4MSwgMjAxMy8wMy8xMy0xMjowOToxNSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIE1hY2ludG9zaCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpCOTdEOEI3OUE3MDMxMUUzOEIxNEZERTM0N0EzRjlGMSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpCOTdEOEI3QUE3MDMxMUUzOEIxNEZERTM0N0EzRjlGMSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkI5N0Q4Qjc3QTcwMzExRTM4QjE0RkRFMzQ3QTNGOUYxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkI5N0Q4Qjc4QTcwMzExRTM4QjE0RkRFMzQ3QTNGOUYxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+jGcG/wAAAAlQTFRF////zMzMzc3NCvMx6wAAACJJREFUeNpiYGRkYgQBBhgYIAGEBCO6SpIFmKCADDMAAgwATVgAkU8MrdIAAAAASUVORK5CYII=);text-align:center;overflow:hidden}.fileview-image-link{position:absolute;top:50%;max-width:75%;max-height:75%;width:100%;left:50%;transform:translate(-50%,-50%)}.fileview-image-link span{background:#000;display:inline-block;line-height:1.5em;padding:1em 1.5em;border-radius:3px;color:#999;font-weight:300;overflow:hidden}.fileview-image-link strong{color:#fff;font-weight:400;display:block}.fileview-preview-link img,.fileview-preview-link object,.fileview-preview-link span{position:absolute;top:50%;left:50%;max-width:100%;max-height:100%;-webkit-transform:translate(-50%,-50%);-moz-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.fileview-nav{position:absolute;top:50%;left:0;right:0;margin-top:-1.5em}.fileview-nav a{display:block;padding:1em}.fileview-nav-prev{float:left}.fileview-nav-next{float:right}.fileview-options{margin-top:6em;border-top:2px solid #ddd}.fileview-options li{width:100%}.fileview-options .btn{padding:.75em 0;border-bottom:1px solid #ddd}@media screen and (min-width:50em){.fileview,.fileview-image,.fileview-sidebar{position:absolute;bottom:0}.fileview{top:3em;left:0;right:0;overflow:hidden}.fileview-image{top:0;padding:0}.ltr .fileview-image{left:0;right:50%}.rtl .fileview-image{left:50%;right:0}.fileview-sidebar{top:0;width:50%;overflow:auto}.ltr .fileview-sidebar{right:0}.rtl .fileview-sidebar{left:0}.fileview-options{margin-bottom:6em}.fileview-options li{width:33.33%}.fileview-options .btn{padding:1em;text-align:center;border-bottom:none}}@media screen and (min-width:65em){.fileview-nav{left:1.5em;right:1.5em}.ltr .fileview-image{right:33.33%}.rtl .fileview-image{left:33.33%}.fileview-sidebar{width:33.33%}}.search{width:20em}.search:after{border-bottom-color:#8dae28}.search-results{background:#000}.search-input{width:100%;padding:.75em 1em;border:0;background:#8dae28}.search-input::-webkit-input-placeholder{color:#404f12}.search-input:-moz-placeholder{color:#404f12}.search-input::-moz-placeholder{color:#404f12}.search-input:-ms-input-placeholder{color:#404f12}.search-input:focus{outline:0}.search-section{padding:.5em;border-bottom:1px solid rgba(255,255,255,.2)}.search-section:last-child{border-bottom:0}.search-section li>a{position:relative;padding:.5em 1em;line-height:1em;color:#fff;display:block;white-space:nowrap}.search-section a small,.search-section a strong{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.search-section .icon,.search-section a span{display:inline-block;vertical-align:middle}.search-section a strong{padding-bottom:.125em;font-weight:400}.search-section a small{font-size:.7em;color:#999;padding-top:.125em}.search-section li.active a,.search-section li:hover a{color:#8dae28}.search-section li.active a small,.search-section li:hover a small{color:#fff}.search-section .icon{width:2em;text-align:left}.file-selector{border:1px solid #efefef;max-height:15.4em;overflow:auto}.file-selector label{cursor:pointer}.file-selector input{position:absolute;top:1.1em}body.ltr .file-selector input{right:1.5em}body.rtl .file-selector input{left:1.5em}.main .loader{display:none}.main .loader:after{position:fixed;top:50%;width:3em;height:3em;margin-top:-1.5em;background:url(../images/loader.black.gif) center center no-repeat #fff;border-radius:50%;z-index:10000000}.autocomplete:after,.pika-single:before{top:-.5em;content:""}body.ltr .main .loader:after{left:50%;margin-left:-1.5em}body.rtl .main .loader:after{right:50%;margin-right:-1.5em}.autocomplete{position:absolute;margin-top:.5em;width:auto;z-index:1000}.autocomplete:after{position:absolute;border-left:.5em solid transparent;border-right:.5em solid transparent;border-bottom:.5em solid #000}body.ltr .autocomplete:after{left:1em}body.rtl .autocomplete:after{right:1em}.autocomplete button{display:block;width:100%;border:0;padding:.5em 1em;border-bottom:1px solid rgba(255,255,255,.2);color:#fff;background:#000;cursor:pointer}body.ltr .autocomplete button{text-align:left}body.rtl .autocomplete button{text-align:right}.fa-fw,.fa-li,.pika-button,.pika-table th,.pika-title,body.over:after{text-align:center}.autocomplete button:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.autocomplete button:last-child{border:none;border-bottom-left-radius:3px;border-bottom-right-radius:3px;box-shadow:rgba(0,0,0,.1) 0 5px 10px}.autocomplete button:focus{outline:0;background:#8dae28}.autocomplete button::-moz-focus-inner{border:0;padding:0}.autocomplete button strong{color:#8dae28;font-weight:400}.autocomplete button:focus strong{color:#000}.pika-single{display:block;position:relative;color:#fff;background:#000;margin-top:1em;margin-left:-15px;border:3px solid #151515;border-radius:3px;box-shadow:rgba(0,0,0,.1) 0 5px 10px;z-index:1000}.pika-single:before{position:absolute;left:50%;margin-left:-.5em;border-left:.5em solid transparent;border-right:.5em solid transparent;border-bottom:.5em solid #151515}.pika-single.is-hidden{display:none}.pika-single.is-bound{position:absolute;box-shadow:0 5px 15px -5px rgba(0,0,0,.5)}.pika-title{position:relative}.pika-label{display:inline-block;position:relative;z-index:9999;overflow:hidden;margin:0;padding:.6em .3em .7em;font-weight:400;color:#eee}.pika-title select{cursor:pointer;position:absolute;z-index:9998;margin:0;left:0;top:5px;filter:alpha(opacity=0);opacity:0}.pika-next,.pika-prev{position:absolute;display:block;cursor:pointer;outline:0;border:0;padding:0 1em;white-space:nowrap;overflow:hidden;background:0 0;font-size:2em;line-height:1;font-weight:400;top:0;color:#fff}.pika-next:hover,.pika-prev:hover{color:#fff}.is-rtl .pika-next,.pika-prev{left:.1em}.is-rtl .pika-prev,.pika-next{right:.1em}.pika-next.is-disabled,.pika-prev.is-disabled{cursor:default;opacity:.2}.pika-select{display:inline-block}.pika-table{width:100%;border-spacing:0;border:0}.pika-table td,.pika-table th{width:14.285714285714286%}.pika-table th{color:#999;font-weight:400;letter-spacing:1px;padding:.6em .3em;font-size:.9em;border-top:1px solid #333;font-style:none;border-bottom:1px solid #333}.pika-table th abbr{border:0}.pika-button{display:block;outline:0;border:0;width:2em;height:2em;color:#eee;font-size:1em;background:0 0;cursor:pointer;border-radius:50%;margin:.25em}.counter,.fa{display:inline-block}.is-today .pika-button{color:#8dae28}.is-selected .pika-button,.is-selected .pika-button:hover{color:#000;background:#8dae28}.is-disabled .pika-button{cursor:default;color:#fff;opacity:.3}.pika-button:hover{color:#000;background:#fff}.counter{padding:0 .5em;font-weight:400;color:#777}body.loading:before,body.over:before{position:fixed;content:"";top:0;right:0;bottom:0;left:0;background:rgba(30,30,30,.8);z-index:10000000}body.loading:after,body.over:after{position:fixed;top:50%;left:50%;z-index:10000000}body.loading:after{content:"";width:2em;height:2em;margin-left:-1em;margin-top:-1em;border-radius:50%;background:url(data:image/gif;base64,R0lGODlhEAAQAPMAAOfn51JSUpSUlCcnJ0JCQtvb27OzszU1NXp6es3NzaWlpfj4+GBgYIiIiGxsbP///yH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAAETBDISau9dYXBh2zCYmmddAxEkRElYLCEOAnoNi2sQG2GRhmDAIWDIU6MGSSAR0G4ghRa7IjIUXAog6QzpRRYhy0nILsKGuJxGcNuTyIAIfkECQoADAAsAgAAAAwAEAAABFWQyXKUvJiREVJmi3AMZIEVGzkgIABuhDIcwBIQ4YAzgfAOgsDAICEWOaQPSDVYKElC48Jo4Ah0zuBPsCCwACMFzOliIJgE06WgOnA/CSFBeVEc1IwIACH5BAkKAA0ALAIAAAAMABAAAARTsMnZAKO4qWHoEsEwHOMiFYSoikKzpESXiEcnDITZ3IgedhJBwWW4DTKSFXLBMpgKLYlhEJAsEIPoK9swkAaJhQFmMq5wQxfpGxDopAMFcsIAICMAIfkECQoAAAAsAgABAA0ADQAABEYQyEnBWnKJMEYQV5AABdGdA8Eli0kYkmF2iZBiGUc3BzzptIpQkigWcRUUgmI4NACn5aQ1EEA7r4vsBnXMUISCZKnRfZARACH5BAkKAAwALAAAAgAQAAwAAARUkEm2RBgjiDU7IFg4EEU3IYTBGSDBTSXVLaDAlMmAACajDAfCgXEBbiSGIkbxCwUME8EBE2AEp4oeADFI3BiC0Uuy4MTILdWCFRjDQKIRr1cpatwRACH5BAUKAA0ALAAAAgAQAAwAAARTsMkpxghizbmEaoZlEcVmHMOxaCAxECwjDsi2uIIEKKhVTqHAxpIQ5IYDJEtiTFg2gYFhUxAdFADma9lAzBiS28ugWaEO00nBJZoaMpuwIDo4biIAOw==) center center no-repeat #fff}body.over:after{content:"\f0ee";font-family:FontAwesome;color:#fff;font-size:3em;width:3em;height:1em;margin-left:-1.5em;margin-top:-.5em}/*! + * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:FontAwesome;src:url(../fonts/fontawesome-webfont.woff2?v=4.5.0) format("woff2"),url(../fonts/fontawesome-webfont.woff?v=4.5.0) format("woff");font-weight:400;font-style:normal}.fa{font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em}.fa-li.fa-lg{left:-1.85714em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before{content:""}.fa-check-circle:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before{content:""}.fa-arrow-circle-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""} \ No newline at end of file diff --git a/panel/assets/fonts/fontawesome-webfont.woff b/panel/assets/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..dc35ce3c2cf688c89b0bd0d4a82bc4be82b14c40 GIT binary patch literal 83588 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�>=olG4!T4BYNCc4f{&*Kgc`m= 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 z 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 Ak^lez literal 0 HcmV?d00001 diff --git a/panel/assets/fonts/sourcesanspro-400.woff b/panel/assets/fonts/sourcesanspro-400.woff new file mode 100644 index 0000000000000000000000000000000000000000..00b37033ea92c937202220bc98a8adb3b4bdfa50 GIT binary patch literal 75420 zcmZs>V{|4>7cQJkJh5%t*2LDtwrwX9+qQGZwr$(CZJa#sch0}B*R^}?Yon_7s?}Xx z)$J-LDk`U>BnJdcmI(w01OxnV~W@4?7s3-vt5Kbl# z&`1yvFq~e)3<9UHqJ-E_oG=g&iYgEgff~n5m!+7nh$s-y`G2_!Kb=shQK)B#$;&AM z0lkI-0b%$80ZCIqskYvVDJl#8#BBfp!BzkPVOO{|C85hH(lhAJn<1pyf^`-b}-RO7<O(}6bGnq2fX2@aUdXh zAW(H6p#Ne`c&0e~{F40qN+T%!{rq~d1(}B!F&L?UL|WC;($rJaQq}(9xa9H>gP?G< zakMgVFi?EFPd{AoV}pR)!-@)kfe(!ma1KF8EB;IT1;Px(A_9x-wzjDu6@#tnQq{z| zU(0hx5N9P_2T7OcM+>`hp%TLytoaA1Ouf=%g3KIE6N@lIDIvuu>a4evifvPrZ53SI zIKetr#HWwOpXLt!mr)8o@&Wg8wkCZ1XDs-X-VjqeefbsdbLO?zv3>cPeP+(($I`0( zgfAVRNyq*N==Q`5?=gre$^LWJbY^$lZPr*OK8G%+Fa16~R}RNF-9CI~xAT|O5WmBh zm#N$kJ{SMi>BtN5@wCg7k=M1{mnnJj?eNs}RX2m-$II;}JJXi^)t2t)Poo9zb=Onx z7~chv*Btm;-==2mj>ke?pCRwxMEkhM^mrdyL)V=6{2Xz&@3POmQkdJN6R&;=d@rh) z-!reT%9(WF-itff)OG zykteXWgyZ+=SG~%6 z>-&Us?t4oo^|O8X_l9@rJdL=4u?aj6*2Oh{2Kg&T82yRn{rIP8+_xzCYXc_uF~lTb z5}Tn=2-y8)5UG>Qt$=x8Jsi$)9rMA1&cI(j|alPoI&ItAx-#H4uW|U)bWHv6L1hn%{kP;kSodFeyPzB zMTx{R$qN1xpMs}rNRKZON&nD@xv%IYN`{svKHhw};#f?DwtRF7$F2zHCvyNZe3HW$ zs5kysIgEfOBEH^gFs9r}4M`>UMHXH%`?JwI$#L9*nlAz!iLCQpJ#ZDs zb{}uHzt<#p;0Z=iz6fQ+7D_uNN(UfNp#)>0mos(e^q_KiC-QC;^XYR8jiIzDf|J3U zKvvvlB0$0P!?M@pjjnd+gD64n7>*hZVH&Uw6KLMs)iWqn5|l)4*9YN2*G@i-}yb}=oe@!SF28pruSzFgEBR=u}qszaGD>XNgYJ7NoSj5 z5%ijlr6$HY_TY&LU|i5B0)DM4nS&OWSmD4xMlXpNx6r8l1>)+qcFQvm_$H0}=xL3O z50WJ+F!Ttk6l4Yqo%bhG(O?fp));ivy%<}?c9oZN8gVZ3CRHD8V(qEJOrjIWcxN1Z zS{QAKCe|_c;9%T?h=ml)ovi|JVXWnKctzN5pgH4zmN{sYE}uqV47@@@z-Jl)I+-wB z1^`KzW#hpjFC~=)`UR>McF`UvCJCo-L=P1RcD^wLqj^~9|0fv^aFG@;yj;C$k(H#n z5Z_!v5Y>p3?_K!Y1)mWLjp}B0GaZAn~t4G;2E_ z71b|c-;DJGc3n}Qj)()kT{_!n;Gp0zjU_!`5p?rEu(F_}hy;fZ;>Q?=X3W2EWtrF# zmS(|GihGb@(}qki&u!|tJ56{*=ek8c<-k~B%h(1No7oM*=$xXRtwGx$MV|cf zEs2C5PS83itT>*XapwjQo?a82M;ms%yYh@DOW}A~IDmpk+uP za!N5^N@I)?2{|yyysWm8NdJz>kfzKJS~u6-|DNp~CIQYp?w^`=%T~r`_p?z3si`n2 zU^$7fLi%8C~L^jjsmnEZgUbl6FHQq}TkRb4P(qx#Jle;pYIW7+bY3 zkH;cbTiJeTZ_RhzyvzET{&@d$2<&4SGuLTO6p9IqX6LMBCZ1CHvxfKPX(RU7C#tML zHsDQYkrUU6o36^)Lu|9&1@J7$JKUy|0~Z9yLJW+4>rfJQ)`|%fNGm}0vgW;Z;x1T* zb>6^y6hC4NY+A@=#c__FYOBn{%7lgR&ZO1sWE*(m>f9%Ev6s`v02UDY>^;{%9vm(4 z6Cu{JDw(ZWbdl5Suf{Ia$0vhFl`|KV8j-(}(zwWKXww$?hr+ZV(Y-!&aZJ}>yIW|! z;P(WhC)B73s6?+)9V;?75C{XJb=!T~&@W$?-|FE#n*o|}4!)WVHT`9Sv26@HYvgvt zZrbwC-39t(XW)=F*Ae;~ACWh5kXKFILG*LCME+2D*b%#o^}?;N@3scsc{E5(fGrzX z@J?0kfwXz1QASV?tr87n-KHGc53jVAvDy6)Ij2*8`UBy)UTLT z&g?5IOvOk(ydiYabzJeHo1nnJ&7!Um=*j7#u`~8OZhgq(0iA_+A5A^v+!gda#%UrO z7tJdks+KZs3G?z%xhL`2TYsGGn8Hsy!L*iw;a*`6Cp_vu#~02~$8N$R9zc3+?o!L; zQBxeT?R_Mnm9SAMK~QZyjv&``rnGQLL$H z&B)7^Xuk2;Buo*HQO9fQs$M>!DVo9uc&M(ToP$21Jpt%>s2p;aC_jB*3iK4O33&nZ zEjtR?+6cT`SD$zJqCfRYwY~F1rlnGY+FsQtmiu6n!)0nCn?sZ>>y@mrNLYAG8k-{` zxba6|W>AR(xWS_uOU#N-LV~nO=}^|}%!!Y}&ThR+VM5h}%x_(3Ot8Mu+}Gj}f9feP z!|1=3_@{DdPk-}s5Yi$5WQ{losOd_~=l;h>7}#IaoK@woxf6xw?(=phsfH7#^nZ^= zsdQE*Qk6Sl`9#l)Cd?q~lQAF$lOUN--y24pmgo!tw^C!cvvno#Xos=0x14U>(iM0?a*jM^WY)cKEA z(Xt#%S@iuN%^jw;$p#zGYf4my=b9|G*w>7)7ptds!t{2TC0&%;W*rCm_N<_r#{J=s zn+1)yr%0ijD%m-v(h|r(icpOz$-uW0z01;&Gm3i3VMN*i=G|ub&8(+~j5+sL_ zZ`|4;#=K@lmAOPGFL#;y0Mkh+S3DiX_Z}2x(xl&NQWdgBp6XJ?8zC4dY`hv#291rr z{Bb3@op}12`^sTJ zMMXE4jd5wb?{8GdgUh}2)s>^3BMl8Zl55MC=AS9=E7!PVn%zjL+q^6TZe^6eyDA{n zBJ2us+QY<}pcy}dE|Fo%tGLlq@$`C`ok*#TBR`&`Kj?TW);!u2plwah9~a5vw}#J- z9gQg+|MW#>h$|gB02B^>-JQ2OiE(OLE?+soYhSZnl+{&B9N-w3q+q(X9TmwmEPJk< zr_E5%;5foY7++P4j3Dm+HQ{7lcS${0zvXgK+dw^U-z&p)#FcSRI&@rYrya7HYTq16 zz|_P>Uz9;dJKak!-DWO1y}7 zd4v%awEq~1EDjE7Bsc#~M=IKMm?s)H$;KSJx?9|l6$Wu`+WLk$a3b=Ru3~WXuhRF~ z+xEKM`IBv$bbA`J(7vBi`2N&eCxR~{K{`-HuINa;e=rF*M*hwc_xq z)0^MJCOVH}W8=PtBtUa$nx4dyq&YO%H7u#<#CCb3c)co{A4U*U^OZK|^w(A8y=3Rq z;pyyNx`kS`?|Dn=@%0f-H$`3L?Vyad#V3uSQR++e^Su2njR~R43GHT!zLAoCmZRMfV?F=Zu~9SUVPv?XEIdrpaoYLzbCzRsCd#y$fzLgXikGur zyC2Vbj*fe{&b7}{``IL*v3O54=WzD)V&qit8LomrGxCc3gRatR{OI=GFYf*~bBI@U zzwhIp>+-h|W?Ub-l_6BFR}Y78LgpLPoCjHk>quivOrZ~w)#}5|fW7O7OH0xENlWcx z)jQb`@nRp@cagPtcZ)C6r*zj`M)WQI-cnoUDzl6$DKJ(yx=syh+vq{?OOK6nXM8I| z!0r9eO?}2ZZ9MDyI_*=4YOdXcf(KQ4j9<@c0njOA72do5XY48z?^l=Bc~(%q%2 zmMhX%L}DU!{H4C&R|eKS4=tUn`qxr9a|Qdf34o~i-gjV7_h%Q;Z7nO=)h$sOZD{-1 zi!cxL7ZgduYYX!zi8x7v83{@fQ7!>OY#HP)EcL=?9pR#7ZJH5qeP{Goq+~2fioLzD zs)s+2Q-Ppr7KLFAw?-q;H^XnkKGXM88+Sb0o!g$%pIIP1{`7!Bu*uy66Yfq5cq4t1&I#vua=~EP2dRD=n;(3=rNO=U=a)dq3K?-I3;C~5oWd!I!PWjmp(eJmjL&GYc;b(S}KA~6(k60L&~!D6(BjWY!0#$r&lZ%S6j zsut@tFB>_)5BdnVQLhGabm?q#0W)u61)1?C0SBy=gh zzQX7N%ROsj09e_)_Z8+!3BwLE+G#xx2}yETbV(>@qaF-cOd z?y~akOM#oZnsOKEN>tRP0`odW*8X`@bG3D_bu}%chcFmNdv&1eb%u}e+I8q@)=W|e z{k09b2xo03yt@cQz~qISBOF!@Ve^{)j`bez<@o_J>tTD@a{;pLy-mBVY_0vE?dC?` z%@vDZ5U$vI4|n((>q@|{z7Eyh$#{Kad%0228rQShQ}JwcwX>15MQkeM*lt0u*f^iD z+qv6$MSG(&zvXV*#lD4Tt?%-CikmND8g^L7G>Y#Hj%n2MtuhUWqEy}d5EW9qPxXus6 z{wpj&Z!)s17H;h$d8M>=SXVvwU3JGV&ise)JIdsr;1|77U4_0T>9=Rl#j%)HA&Uhay2N>RzKm_a@j zp}-a4y%oW`kwfBbm2WCSk^71mgKm+>^Qk@Q(mldUeMV8$5*1OD6#?)sG<186JbyDP(pFTupM zIR!VgV*BtFf%L1G=dWmN%*w%|9BDL)0rBAuGq96lU`&ET<0!3^&4^s}$+% z{*`+w5|cQGmEx!LAV#?`DRE4qWFx{(1hWc4W&G5J1h^Q&>lOyAgGx*N@6rP-d_U{@ zKy?pj(|W|C8b*~mZk1c-iX&`sQ(G%Ne5>8|l`$WVfl`i%XM3gVGl{9d5a=78Dl@u{ zvW4zoGKxQnlYWzlPm}f_^t)slJ7^kvY#Qd14@_6N5JIFD?IQ8_P)rFk>h04V3p=`XO7t2ZmZ3>MlY}TM6*e@o7LxPg zX<&^KX_nsft1FTlHC%BLFLZ&sfn*o|?>8=>fNxFANQ@dFAeuhlv!6LNv)xq-MdF@g z1KR}6M9&WwTz^@gaU(5>pqEBnScEhTQZku9e>Bt|*l`Mx!C(rQK~HcnkTYQQUK;)3 z;t>3LD?4pdr1SXwegE^s>DftDSylB%9^r@xl7QmGjZjmQE5pK|BPAzsswB?YnCJ+( zNhSNEIGU0z1T4)HN*W(*kr(s^PvXofi>wels~eq`wuNezJ`E-{;0I`^FoF1D{)*QP z{sE2rhclDQY6igt0IBVB`00a~O7y#n^vZ%7KX)dQwyGV9!>#yr6f%!icVJ^{hf-Hi`B-wb`l&K`T~wvH98t|f>$_R@5F4=g z|K4}LUc`A|@I%OVO5ND_5Ocz(`_T8OZysOByx@H>_@i?M>35fJJ>7`Ee*4Dw_GK9T z2_6?uYm}yaS6`A@Qd|-{BD_z4O#vGp8Zj`EsDn|@wyLvgv&yqds!(duZjy6Ra`~%5 z-A>a^>`CKE=t=!9RGd^>=Nl<3IwXoF+9b-4&57~IcE|Xe0m~T0IMIlumtx&4eio;I zRP;Qm3G0%P%gSl)#DCVg077&+Y75hw?ad+*JL6}W+{}1obzyX2aAAI7i(|nv<bJf&Clv%`7zp(7K`N2mu@EHM1~W#6^PE5 z2uVm3b{iO+DlScagg74hH{X9nObN*}lzISnkMY*uP3l9yOajqdS|gUKj(0W6i3}pv z=Rn*6CruPBc7F8rz`=o6UEqqid}RK>_zrGWyo+2Nd1GK>V5l%kf*hGVp^I#bOh`sh zCSN*U`cB#;orY1}v~JL-c~n0EDyy}!>-n_V820fjQUE8v)x1rCe`BY$@Y94D|V}87tT^c{*%iwLa zVe~@IQgjQ=6wSQ82l4erL#Cnn|Z8f7c{gM5i4ch?CjBSl(%&J2@ zT|Ii0r2f>zaWXlzl8u_ZoMGM2W#yt}RlQ!y#B~BM*h zqwBrEk>63lgyST6#x$F*QP+}J!K=-^(9!V3bn0@NE_=7>mf2VN>-AAZ&`RG* z-&}r{h--uoS^$k8Z9Xj%&2QQxjf$ETgWBo*p}qJ)kcbqt9GWkMRH_P8T$JeFu7i3a zxdshHl5rIy%F-%{6i1ZFR7(_W6fTs}lv5OED9fnNC{f|6;m_f6zbo_8ekCR{Xp%Ybd%Xd6p3`Ce;Bv{@N(%S`ZwHz~%?b_m>h_ps)sGiIM)rDoah5p#_YUSTFFE zrE`gC7hEY$mp(6`OX*3dX=tozxHLF5Tv(pkoZ4bqaZWMKGA-AwICLHU9(NeWO@^S2 z(OlBls`IjX9lDc~N@mMPb+UXjUCX|b@to~HQE+0(5=5VwKM{Gr@<8s);1gY*Z9V~i zBmMOIG9YbaYOJfZFg3PaTpt^^NL{2TH!F84|5bjfa$l*UmZYhycG=8jqHO6rdDgYa zT;8mbQ_WU<(#+^piQ;ogX~W#Lp_sG?p;)||d*AyMvJ+0?YQT-LU@s)DYm zZTncWIIe=PXT_*4-_*j0%uB>QjZdt!HwsfiKYJR!W z=4*MqB%~Q*gS*Jp_2?dKlz4@lZ@^++dNrHglG$>xAnXYtN3X=`*1is%R3QI|LWQ2^ zfK26@cZWh{+jWOfSkZf$VW;y%}n-N<{C32 z85I>7IVJsYu=`&8>6f;rE&YJLQZhUL9_`Kf8)OS z#%J^Wg8ns~edY6uJfOS4^m^xjDG+@X;q#+p+z^szuz(ZleU}8m9tmx0-X0@{sRFDB zP|rZbu&mf~c8W$|hsMCi%}OZwQv@lMrry-Jz>XwB9)LiQULa7%AF>lvL+2MQee=It zb1VTqonP2Gzv@!{UC7|j8qA) zFCA3EtsN9v6T>Ptu(>CvnW6nZ5Fp^c&w$D($!bw__sk@N1rL!8neQZ~rhzFEWr$@+Z&tQ=fctVeC`BS=knz`A43Arpbgl?$+aY2jC@@8^x znZUQ+7Gp3M0Hyn7jSi@=jogy>hcMC#d7uTVfI?-73?Z~a2%gKO=uKE8vVuvXWr+IqTwz5T$q*8Sh; zgl+>=f#r&KAlA_Xd#EEj3+e;pZ==U&2MK+wygpWFx~~*`K7>n-%yZj}B_H}CelL17 z$t%WUF9UL_JQJ_}>|<}l72CEDGh|JPiR68^wdOlHigZ87C$7*lQv{@`SB zj8OQ1aitJNo@7csm!Lf8l1HH65}4j_d6CQEW+(Vy%Dx$M*Ry!7%0xa$n07~qjw#-?`rVSdj;$VS=x2nuuelSBf0%&(JBZ+@6Q+lD*AE! z?y#fHMsLW{BIsGhq4%1+?`w3^c>y1WUfyxrwaiAZBE9*~(jp2?AIu&FEcxR=aobg& z85kOt5kH_5hFV9XwIbI2tywmY-6g#}x zWY={@=uOk5t(ryDA^rsa)pD=Qk7P;Gp8SomRd|1sqs)^b{woUQX8)Ba&@CuPZan+80Z9$rpK zdBXx~K>dFW{G zTfF#Vq{eyZwx@}OsWK0o5u^1KpPs5t7Q({zc?B(;q%u@DvjzEfPAH9Nwgjsz=$$;E zviNAn9bSt|Gcb#y1hWmRr6K|gS0qAXa03}3^e5ECEi1 zMPuMSyk?mpd3{bWBk_D6U<9V+A7F&!fINZ=OhG+F^T`o-1x}zuYrdn z;kjmUN82xR?qfXL8i+lR33rz0LwYJ`qCe*+BWUJ>KZ z(JCM{6UENEUex}^)Y$qUaOLf>PSPpd@>tv2o_$(Uy!{d_+yamDI2Sqlyj*B|7ZMsL zAcY4(#{;Ky$0|H9kBtWiNM>h3cEJ9fD;v4h6a-ypluAQV_5w7i&*u*JYTsqc~A z|4_c59g5Wh%XDs@yPZ&gpNp>fFI%O@RSIeb8RkXQ3K+cB+qJYTXKB2bfIdLS9qVCi zJXci*yz`E!=Ga(oI>J=0`vl0=r->y>K)nNI?c6wZaGW8cSs%?-j8}K-3cMF%C$n`n z?yWKA!^Sofwu1xv$boI*aAFp6!4TYsys_u{6hkXCKwnB5=A#v(a3&%E`VCH*e?6CmMlb6g_A5v8%*c!rTcavigb1L#HM(%RZI>8kUP$EDJeRx&)xPF`m&F0ZGK*ie~ri{~x ziyJg1QR49%HRm4(r`zwO6nS^(B={VzHE82vJNO+U;dPz;`}tun#G~)sEysvW+^qsGBA@ts_ z4hXt$w}*ON569z#UYv>aA-Bdrx^Tzih+drW^dPTHq4WW-Oo8-5uMQ}>!MBHIn?*vB z>9MwFgFS~~Tmp8JP{AfQ&0QO_i|ZmKS_Lgeyv$UrD)bERPTEOn2!OPI;Q5M9k_Y>j z`};GSTU$CWJi~O1^K@f$&_(w4$oVXzF6Or`SGDG|_AY*}Y3laKOqU;j4d+-+&9Ufg z>!qX{$FcBD(IXuMAJ9^RxDY)otYTQAw6U;eR|aznQArK#(>8hM=k8xYx8STByj825 zK741cY>uK&S|D;qR8XiT(X-I7 zBQ6J4_h9#8_Iybl!u}#rq|uco^CWpCNfzQ3c^CT3L7F!;$a|=Hhzm%HxDo33)Av2^rrd_SIDSM)(-0daN>h%<(k4|V zUB=NW?nC6Y-Ar9&j)vWy;!+Psg&4l9N~(SnXLc&5(?cEEyGz!X@yM z4NF@{Z>4_d-wiHIG(?%JDx%K|6c{#^)yU4<(wMNa~FS)L>)M^ z{@oJTlDVRCO?e;rJQTPSe*N_-@T%~N`YQIiZ9-;ZQ9otEJCQlDG0``1H>r|l&BAS^ zV(PMf5w=Qiay{w8{BAXr9x-8jlze18!JYohif_cXz%#`&%Cp(A;+6f%bnkGFdlWKJ zJ}H;6oUY5}YyK4z6b5EJyOduL6cN-uh!7zk!HC9)CV_^D=8l#?bEH{OyJA&4Y0xc_ zG^if&j)qV3rG!CAA%ZP}9#oK@lAqJ3xC_9jf>ai!FhEHQ#|%#jcMhKnw+`PAw+vqo z2ZXbSZ=-7bmS-A^%%>z>jB+RjSJ+izD#|1KJ74;`@Y{m6F>X!rGWY2(pwx1~`V9Cn z&_m)=(z}pPQ6||y3VG8NNt^bPc9Z5^-9sI&4#x!32-Adf>t|zNrf$HY^bl+O@3?jH zJ8e0woQ6(KhmHI4UG1UcID4}1tif7hQ{LqXoRjNX*p&=?q4z1_10({P_9Q zQu!4w>{$uf?mQ9JlfUQ;7FiF+5~N05=L|pQg~T6BOIXiRTPgMZVNjOfp=VzcZT03a zYc<0_SD4+#&rjqF2t%l*ZQDyd?nhYj6n*$yFqUq&=gsvtKQqaQywA3>Q5jds=}OMd zt|b$~E}w(++_|`njmq;?wKuRSxcREyw%v=FBi0q<4~!4Y0R5KCsL?aTInh@)Ms3$4 z0w>yfW97OM##(hlrQKWX>5|c7h9Hx7etcjkalXo+PwU;Zt_yWcso(*3_n@ngLRy}4 zGUAweNsm3f3M1>&3D`1ta+~h;%HXfHC^h^J@^<~Z776`?p|yztXliI#6uc`)cD42$ zh6Z;7`(J3W+&)&ZWhHjI{*)FD!$aakmO_$}Do;5Rile)Lha1P3eIEsZbwE_k7JM~M zjnw8R0a}~>L9x`>{qXZD& zh%(5I%{kx89~W1JgGBKzUyH+qfK>`EplSHDEGKs0o9C_(t>=EvId`>=xL*Sd!zgGEnmzaKMKVQ?5HXYIt?)WxP*NnusYK%DclG z^+Yz+RgL)cWk61f642%7l!iUy#Zf&E?wUd7;tchwq5Uyr`#agT8#O6se%aQ}v~uYKPXgHbx!w~0b1$acXaueZXIIRZ)RQ2g~`HwJ(*sY-0!W<%!=7dVrmK? zu@xsJL)(lj7csyPh;Jg_JFf)olyFG4UP$Z&?^S{y#=xiZz8E4qO;;!McM$fF$J%qP=6DrPd8 z+Yk?)@H4FH<$fgTtZy)oYrQPJFcjGxd$~m(5-Ge`GnCr=hE1O_TpCEiRl2HJY4N;n zx)_KY#VlQIT99fYmnOo(%jWkf<+2t#5M2*qI{Q*NnE=Ioz-tK0)%g;DvG0OA)(%!* zMwx+fS$j27BYt!r7xb8jqu&3L7uaWSCU|mCv;Kax2rTRDYHd5}x|`-hS0C-!4MQxj zjtMq+e^#~OEh^*3;dPQXVp!L>q%Yv>uVN(x;SK0}hlg2rWRRuBpS1f;GB}cz!F~~l zkqbID)I9amwm~3zuRpaY6C! zgfNj%JDx5PIY7Ut&+zLqeM%;QeOF1GfrQ+jI7KffBi~oRjIrgOzU40r7HJ0hOq>{f zl*r#$FAvD|Kbphi6%=fZZhN4QTp0ent?a9sZg8H5W>`(e-%HA3%CG<-9NggO6Lto# zX}*RNWoe<4Ssy~fzeMCKSNJiP8#UJVbjpiOi3m(J?~eEoY}uZ!bV&xTh^ z3wHioM1Bp@GDkwsj29NYUv|-oJoPJJwkCQFz~U*sNE_W%Om^ zZ&ip@HfvNdE~GZFaIw+6?E);Dr8XuH3j*mNuE5J9520-v-e5qo(f+VfkTP`J=M7{X zXLruAE4v%S-AAwiE#8vxY^yy9#lA3VeZ|1!{%iYT|$+T(rx(UD)J_q z5{mU=uD^LB zO@nLYtH>onF8OyoN1tU8Z_yDeU?;oh?Mg4i7vtKc?^YJyhe{=2 za3w21iQySsQwVOaIg(-aLr>@L7O1xjYL3;}5T{K;u1EToQf$>ag`BNi|B9;_2nrjG z1$SaK;ye-TAT)G-Q;U+WB8#PA%cu9SG`jZBj?O`z>(OdH7nXKXYxXYn{a*02T4Dri z;Qi$duZY0*&HJx-r2#Vtj3Fr$DlL!C-&T_YInK8UJ(`TkK&({M91e#{A!aZH+NX-M zv81#*^s>M?t|opz^5Y`?vwg}zY?s6drO&`1&jSZfyN}q!E_DAPRO_Y(jN=x0c%p{X z5ml_}fc>wnj=Dm3%I;|CzM+_)V=;{Rh*2S-y13B7LWWR=3;tS|%WMm}cCE>J_GHsR ze4Lv1JO(=(knTzFB__HqUI~$Yta1SxXxk$D^G%SM_d{xWU3tP|%J;s*qCD`a&qo z%bI2I2QF^ZuH{~9f&KXq@5d|$B{A)K@`j2^lZ>w#Ao-XmN-~|1k)EEfkxW?R_mgIF zoIsuE8uo*F2ingffMC>pdR)VpB0D=nfyE_VVL>s?OWoxx9;$3APDsI@rG$Ssq?;I7 zDjeD&K~Xk7$i4Mf@8Bbhx|0w$U5Xcpxm=II9f=aY{}MT>-CNPk_(BG+(O^F7&y8gh z$x~cb#^GiWm%h#>0c`cqwMFiW%BnWzBqb#7K*4}v+c(=UO zuD+3c8N*&@&f?@oW66<;ixeMgQGGQmo0qpujoF$Z5GN@D2SfT~4b58=FGM+y;StE4 zH54Bb%d$@%bT>z$C|OsHY-B&{ys}RO(;_fudV^kri_Rzm_t_JBXa|I3k%Lotf9>cf zv4WU9Cr&^pPJos5M;wvHlW5gG2Cuc9?6B##WX!f_L9nj2It6xYIvPgGUk?K0tSdXd zF{sk_*gy$}QhrH%%B8xWUyBk}3g2mAJaT2hLn0C!;}FE#96;F4 z6m0gT4kPNw$?;c2ZF)qXWKYK>lW3X>rdzj~?xF<(uM$-gN{ieza{{9IUg>Deb$ zWC2jxsOz!Ra?|5djijzg(cFH*@aFzB1Z*S;a@3GaVw$m1Th4Yc7$Z#v)^splrF@jQ ztX~TNIZxT_idE(^?e<&3Jc~OtY=7lFb_MYo1bXv`+6hj#QEjwQ$@zC#!#Vu;7}uPZ zCuw&=!|j&BhKs{JfryBEXE+2(l=$D7r2U& zlK%%mK)$~p?LB>Vi8>5VF3G-4EZH0U2yI=?+~?W|DCOjDKKC(v3`MA^sJs>Y$oo2Z zeQPZ;m`ve(m&H5u;F zhhzuTLS+tFyZqGVTII=6Tb&lzP7BA*Ks!Q?vZXV#EBI_l4qfm6PSOYp&Lu`|>AApm z>KN?oEH0IrW=n>{4g@*q7c2mmOCD=|=+pRaXutM13>`y%ofK7Gggq~yXoHJJ&O={< ztb)E5MKHZq0!1WLqx|y$Jc5zX#p)yeUaLl}IZP2+TJ2+)+CY}0p!@h}*hYa)UT0ZF zMaAkZsld-58KLHY~Jw|F@(1Mg9hEkYl z>@M;eUy!dowxg1NLqywR-G2X zDVHQQq#sJ>t~R?iXkAG-mO=4tdPKksHA+Vw!gn|+ga{;ey3R&1zm`Ce`#Jp6pD2H# z{$abYnnD7x^A52y2!|*OISH@8p9u*ZLei@ka))L^aF%}YgdduuwJl$ z)435D4mG(zeRB`A9$c70f9ZH*(6R$hvV|SFpxo3VoN>TynE%EQgzNDjCwq+ z2sz(1t4q(IV`v5qJHgcTJJwgM-GPq#*@+}wy|l9IgtB_v#{Nk6qp{<&$bH+GH&7ya z-U#PDJX|k9IzecPlF8Mu$GmfB7Lu;Vp%iU5sXZdPQYV4k$XCddgd$HuPKJXR5}kvU z%1dMgQ%T0bDa?N2?N1~T7|Ig^OuGR;9SgrU3PPUPh=4BFSv7{|u1ND_!4E`e3tZ6rU4Sk;F+ zA0*Bs%0ZjP#Jz*Zy-V78FgRSz$oc|^J%Wyq?haMx2(Ir7Ma zQ>3<_7fx2}s8U`XdB8U~MiYzjwtvC;VvvhcsWZjkiiC@P)#M9;dULNbGm`vG1hX3? z53OKxJ!?(k@1_tB_YyFJkn?^$A{`xW>{WKcwI=gi^1@wc16u{$C3CN$m5SiN%Mv({ zqNV??hD1WNw&{f|L-&IMu8#E=M#|%W}z2dymaHrXXjdJtV6|zt%~9C1D^D zt|E`^sJ@7TlVZ(uTX|YeP8x}`TbjFK!SEpKMK+B(a&nc3mBU#Q@&j|P0%?xya(r(` zzi1Xyb1HrNIy*%hj6x*jo+~H70F+a>;PzZ@VRZp-M7q%*m*YvXvu2G~lXqd8uoH?Z zQnc$bbge_`G_XMdJ0eX!vT^hJZ7QXBQCp=8M}6E-LiLo`j#XTIY1Pu zgrb;^%Ms-{+5tDo(xeqA8y%0yML+~VTN~hE1NH#0Qv~(_up5YGiojkVnr#Z^0I(m3 zR?5I(01k*mWdIxkqH-%x1;7!J=qLb{KvXFQ^ML4r7*qq%B?&kQz%dc10pJ7>T{8yr zMWXw1a0-ZiF$U)VI4u(W0l--x`qK*30f72h#P{(oaBo(O6n5mL37p zn~e0RND6^8jQgQ32!x^~Oa%)NpB3N?QiEsUJ@^wei;P4zqVA&JqFm8Su|#YkR*Soc z!^KJBqvA8-E8+(6U*dly?IqnML6W(W?UJ36(~@hFM#*QXskD_eUb>lD_Bv zP(2fygw4WMV>_{nSTp`J-VYDOm*P9{6ZkFsUs*TV2-yVLQrRilN288LgN%}mP8wSn z#~BwGFE>79e8u=zLQL2ac7#3QL4*<6L^*MYxJo=FzK{gjmrNv=k(K0cCX~r2lN6I8 zlNBbXO&U!8GVNhH(lo_%nduwTFQ)&>P2`^PEctZ#M)_g+Q~5t;CT2=AXS2a(nP!X3 zc9@+udtvsEd3W!+Fw9IKivLy`#*n@|K#vf_)k-Q+W6DupKX2~_H#`KlMX>0)^>Q)(XnH8 z$GVOmJJFqnc3RtMXQvaL9(MY-vsLE~odAaxx@y<^=f9OJV>DncvOLmvCE@!&D z>N>RR%C6O2?{xjATfc6}-4=B_)a_w+yY7zNy}FOGvvYy-dgn^#J1z>Bo-QL@@?G}1+;M4gZSNZ3n(n&N^|qU-+c3AqZin0+ z^!lk+M6a}7vwAi5cJ7_mdrt55y^r^<@BOUzr#=>ae(KY^Ph6k8KI{4%>~pQpuYE;* z3;Lezd$n(4zn=a4`)%*{tiN^tg8tL{ujs$M|Ed1J4ZsG_1G)|H8Zc@=_J9Ke&JK7v zP&DwTfnfs|4ctHQv3o~%fA>`PMeb+an>_k>tns+%@wca)=V;FwPpy}W*HEuKugzZ9 zyqdk+dV6_K^q%cq?tRhw7awDv4n9MC#`?_n+3MTRH_CUCZ?*4n-&elB_#$nHf{WVM*Km>FR=pT?5a3J79z>DGD!z+fL8h(BF+YyoxE+a;c zSU9rvNZ*mmMm`E095_43H0XNpg5W)UyO7Qy?jb`$Mu#MXWQR-*nIEz$q$1>O zNPWn|kmgXY(6G>~(DR|UMp=$>AC)p{<*2Qr&WyS@>e;AYNBt2d3o{RE9o8YtDQrO4 zps*2Pkzx5^C1H!h4u&;`{WY2z9XDDt`q1d-;ilnk;lAN9;c?*!;fdkN;p@X2$8;Z4 zH0Ef;_=u8-O%V-|Mv;z@lOoqdu8XW6s~kII?544g$61Z*Kdxk4O_W*GfT$5sF;R!3 zjz+hQc8pGrZipEfvnSRlc1G-rxRLQ@@e3175)u<`j2}6^HqkM0VdCe+FNxa3zmuGk za+0nl{g#}Yd_QGa%7zIR6H+GZns9YOW2$AUXKHY2eCm?aQ>h=*)M@@{S!vtTo}?4$ zZPTODUuU$-n33^ZW1|_YIiPu!DalOA{4=X{mT#6OYhBjCtQ*-sWsl4rmpw6icJ`L+ zn(VhZvK+^p$eh}oe{+ZCF3Me(TbX+%w>}r<+2!@f>z@~xmykC(Z+_moyq$Sx^Iqq{ zd|7^*{J!~t`EmKv^4H~W&p(oXHordqMgErptAYUqK?N}d1qCw;mKSU+I9+h5;BLY5 zf~JYeiOv)KCytqzI&sOw!xNttl7)_i{R@)|rxs2xoKd*3u%_^C;j2kTle$duo)j^u zXwu3_$0mK4ES~)HoqS^QwaG6hYl}>Z?27sojVYR5w7h6X(c_{gMbC;} z7X4BjSv;qB?G(|JHd8#OOq#N9%7ZCir_xjHr;ePOJay63ZBx%p>o%>=w18<*)3TE-$W;D%|&9s>5F>}Ps(wW<59+`P@=Hr?F%<4MJZ&uW-DYN#?Iy>v`tPiu5 zv;Ai0%w9OVYWAx+7IXT{37a!z&gr>|xl`sIm}fE1ZeEXh9`i!xWzL&BuVUWzc~$c+ z&wDiQkNMbqoB6%xkD0HTKY#w=`HvTf7Ia(?xnSagLkn&%_;n$^RqP2@|EoxpYU)*7F&&A^w=P#bQ_|W1Di=Qw4dx`lHwmX$6$xa`KVUzUAcPA%`VJbL-m z<;RvkS}}aZ>J^t(JYDg5rTxlDD=Sx-t?IEVU{&&}l2wORJzw2!b;{~#t5>f+v_`e2 z-$SrnTx^79`a(S!SR^P1?w$9u7 zXzSMsvx;67<0~drT-YYv=DjU{+nwz;+jF)r*?xNapF09~Y})a7C$>|y(|)J-&aj;m zcdpyH{>|K4?STuTmsjBXYm%2%+VrN5&FT1+=4x6SErS$Wh`qLg3Rs9WN6XmLq4XVS zefm#qAzst|HfxQsG;E;5e)WSS28(b%mK@y;le2%zJqQ>{z=5!=Em1M|Dx6mblddXW zHNvG0V;iW?(3YXJGf#WDXRiq@{lsy}GYe6*BVYbD@}kbq}dApBXBp z4?(T-W8?|kTw3erk~WNdEt+dpJzRZ_n`?Wcgu8BE1qmFs|8ND`!uFkyvp=cX2)vPf zOvBX>$RKt$rEB+~C>B@CKe28%o?X%Ixvisz5T4Kv8N*jAeYvkzSmXYG!M?WI3C&LQ zjQs2wsVc94irNWksNVDjZOI7bw;#Z;6dTV5zhf!bF4xv!f8$_isyeV|>5?+Fyu2J1 z5FD2q8YR$O`z(sk%GeJy+Z!Ih+z5GwumN|Tz(pMyd1iTzw(*7t<~)%sBjo28?n>bz zcVjG%ocmN;pDUWHT`7U_R_}2b+teO&!P~N5vieONn%UoGCa$?(SeN;rE&O8bh|8}q zij7UV)V>!Cb-f!QAA{t+Mw=^HWAy@m3%%FwT(_(AAon|b%=)Bt$?sTi>tAp-5WaxE z@DApL_hkD{oG>{(KMs-2-9Kq-!FEU3*SZG|eb^hA91dkfaMTk?7eb!%nh;fLM};!- zA2QFc#2X;dV)QV=P`{g|e>cPNpg1R%%kD%_KislABaj*Ru`NVQK zhe6DU5WCq8lCH$)gs~B}3x4B&N0KifU>E~U{!-LKtWyK^W;#Lrei)iEgI_an4#IgX z$w&Ov7C~QzN^kf;Q~R8eR&OYW76eRefa4pe$Hizl(29{y<~y?+sQX=su!M*R+gUhO z$qkH0LLJ6P+7R*?(1$3}3+_F1IZBVOtjy8<&eaQc8T6jD7#W-rKUR%!A5TAv`F{0n zfNnVKBt_s$Wk-%3y344k$I0VX?;ER@PvOZSDZkfAxd)V=J-L1F0o$3*80+3BqaHW; z`pKyG(;~3h`2Ml6Wq~Rrc-V$PEKnCi32N2aTNrgQo1wN%f5BZ2!1j!&<{|WcDCvZj zttq;u1=mFH8YPZMjz!R$ftm)YvIjxsg&~z-KgfD8GI{s8*R-y=qLtbJ2@J4$hr@x| z0<1UQgWWbMeB$Uy5!}~RRTGa-I@SYjvvxsqv?WBFnj9rv&VSN$H4W!PbctgGw-}3`ZW8uCG;KIpPmw|(YHOYylD;1e`s(G zrV&^1RAKN8bS0po5{|B-zG$(g3v|+qtqk=sJSAf23^+#k&4v0>&0it0GnNmzHQ$&By~Nl|kU2YLIP2>uM`Nwl_2TcIDpYMP_D?wV?jp?6K^ z=_=yUzSCzB>5S%M+{lr=f9?}O@5;B;61cJHA-$zMHzg@2FC!znbk~jzWn0v&T6*fx z?$f8D_k;w;jt>bwka$|%soCqBbzXwK89(lVZaBBXiEQ*2Xfgt+XbJ>~X2*UEI%WDE z6T!omC2&U5Fxs9O)||-W{}lx#iUry|)O+o`zcJbqr#^d6XHMemPqS$#CwjEOF5yW! znSKL1x@iUN4JDW-{%SfyeP)&a1G?t0Nb7oDqNST=tYtDwZ0&OX3$uGJ!}et<8ugAK zF5}(*!dPYV;g%(!_32je!&YAZTE(rZ=E%ua+}4}^twy|iw8f6$^{Z$YNQ_p6=WO3S zO+6OBMX>43mHZY6z_70A2<`qE^TOY?#a{eBprO-Y5!`)V0_Qay;xCd-PiM*+r z5?d(A{l78aqGc^+&FNdjSo|BH9By2{w3=$&%D&SHvwu4EVdQ4PA#t;>2-uDyoey$CFK#MbnGvjxwt50D^;)d?;t3vy2&eCiG zI}S=%bJ)FDeTg|(zx2$m${HJ8h_fH5F$^rPz-{ziB7a zgM6`>_@H{s4~8(paMc%%K^4Ri#15?$9n1*P6Q^AI01DDgQ=N$^#G}2$nKb63F1UIC z0VKKMbL>`c<_zq|a04hjTMFl#d`5j9$WYDDm-UNc;+f%$Ko13B(CfrH;@kHJ&;_mI z+a8M(hBbWKy%xHBw}9cy`44EiOyqf}H=POU3k2tBy~E*tR5mywMCSH61UebcqqGR} z^NriVhzF!)0_BhzT2Qetyov*^{elKgL!2M0MKVc3%Z}s%zUxRSQD^kmKj{vL0nTwC zDOp|SI(r|c=Oe0|2YuC;#JkY?F7EVRm!BGrVfd;Xmg10y+~{PZ$8GzluY4)7ut8w3&Yw!xYp3HWP9|9Kz1%B+{~H zu){y?k#LeYJMG2`4gPcm#kKG(9-kQ(5d{62J;HZ9lV=q2-|?KPI##tJrr031XLZy;e|3oMTfCLbuy6{yJRB z+hOvhT7I~!*@XY0Zu-XJ3}r_AIO2ziv@(OOKzHU4aq*m@=GiZB-pqT{-yH~~);`*w zg3+u;&{o$xyIBQCS7zR$J{O}gWEB5sDVqC@h5I!&sf0T{i^sqY#O_6F)|aUez&^&1 zVBFAI8JAN$o`pv+i9yAy0zy8q{-;&8^Gn-D(*KtQE<$mU$%^?5F8SiW+t;J-yROv1f%;d`2 z#@b5iHp*zYyovhwhu#cyVtG6UnL!C}1|Rn^9NNPVikTl~HqQLOebXageUALUY??!O zb52Og+>kwU?+?BR?}uvXotq;G-LJFBn^FnHtM~{1!6CrmobG2|qJGy+xcV?ny{TPZ z!+kjrw$ ziBjl-hWJd<;WA1WVa(ScPWLKY7$I>KRNw{!J$X@bodIyXH@CC@g3@B}FK{RZD6rO` zNFVNF+tSdUSperT?woRa=~7QfUPcu9eM=e!K?_*U54MLS8qH;&KpVmxnXV4$A_S;= zG`|FW_nUM2fj+lsIPV-g4~IMc$>Jc8gXbU+UsK^6>UWNzH}Lk?@V4Nbpdbp|*hF30m zNQDeUu<-*C@QQ(c3>97sPyC>hp#xi#GQ4;n^PHD5m&3bWZg@!TM)-#jdOeye&LC}W zq6c3PJHJEh?L#U80Zl+aWe8|WE%HOpk#G@-&k(%(p2SpM_Ro=I(eNHPr=n%}D&lW1 zZ05w9pz}?_jX{2s%{t=yF4(GRHd3AMJAImL+?hUjmnqzU;HZuxM^$zFIrX3oL%qAu zWJ^wH<`GxOfhA-hlINaDA+K zlYQ_He2i|gc}s9V0!z6cfxjQS-ZWbmLVQYNp6l*vpdH$LJDyybv;&#~+8^V(6E8yv z*PGU4fsRx}h(GgqQq=o4%@w!xSGqY3V|Mor~)+0^Io2wQJ^ zjH;|kxYUS6RbR-uuF`+6`Zn`H^f|R09-Le68_96%KHNwZ*1#t;8wUwi4#RD=?l;*( zy1-V5YMpR)EK8sxAy!Qte`W=HH@=3Bw#S;+g`@n`?H%l+MnV!6;Zu*C!HbJt$Q|@# zsNahi>P_96gS*ZR5ERz{q}9eeB?`aVAi&QEP>TRR{~v(VJEHvm07M#s0Aa#qh;y*b z@Eo7A9sKHw!2d?z%?SK&130Jnr=L}GieJOQKLT1$l+;qc0i^Oz0MdD+@W=?*wqNHU z0uPH3g+~C9cv!5Xq2F3=R$H{M?SyHc)*vt3^MB0wiHm#M>LY*4%W%|l2`ptT=#hsf zoGM^;Z#sN>lNz?VaOWfR{qNOV)L*O(>^IYPd}>DYSmjkXjQ$^gh9-w9Lfa1|^lH}l zhg&!_0-vLVcMzBG_WsXsIp@y8boY36m#;KR;2LLo+w=`w9F#DQuxql2B4E^T+Ku=X zN6=h0CO|hp@DM@QL55*lH+lUpC_coV1?RD@94v|#wJv;?Pkk+BMPILvBL0KOeW!;I z5!xy^XL(~QIE)=Y!#TC=9Gv=la?{9ffHMrqjBvvUZ#KCG5dZBav={Vl?>}S9np|~0 zD5E7kLu2IS>rW-HOsk~-fBr>V*oc~toAHlJY?A!366VI%TIHd1IF~6sfMVzsw;#M$ z@_#aCE6)%D(w1ovQ(f3V*qXV!_y){kGW03gohL}Y@1W5DZ9PgqGW0I<&XWsJ)QI~1 z*gwV+Uy5$T=V;$uSG;}#|9wW~y|#Ly{TYXODXqNPr?33J`O@jADR-$5Ex^-pk)w+vZ z{;Gk$%SBOCA&R1=F@ETT&E&)w%mFCSX!Ip29bU51Hp0<2nzOv&=;lTl^=2I&HxNrE z4rOrP;85wCPhj#)6cg!i;xALbFGHSV^;bEahaz$pCUXMAk0nFU`3dy~4gP-Q>n?95 z^B&W3qYQg}4TxgSai6IhZkAyxN-V$c(n|Q4>Ib+!C$=LvE;4xDxRi_CS3IJAZI3)T zJbuOyxWif8kyyg;G5c@(&|D}uecreZC7WfBs6P?-Kd{qz`uo3>f-M(bXwo<55W+S% zZ^t9*b9*jpkfJE{9CNFhXJtE6a|yj&;x4$JH=eJ0TUEE6IYOU0xcl^}m^~pu!e!PO zX!PSU15Ugp&g&1^AI548Auie-#PaJiRfvc3*KTm0+pA5F{y)aP10bqvX_(D=@0x3} zNx1JC-F`6m8PhuC`FAWv6ncP z9Fq6XxeFS3Bf?F4a`UHE)~5y?3}S=h9{-B_CX|$ zo)o-}!Zsm3OPB~7F{~h85dVLG!I+~*k*By4`~{f8)t=%07LgUC2|uvVW}{fe^8bk@ zp9}BLpO8`Q4X0Anji~4FC5QYx1EUw=B6SH1sEPfoIGunuLI<6PbkMmT@NUq7b~ET9#z`o_B~+Rd#C?zgqofi<{6U(B zG^Z6QrX$u@PXZe@W5m}BhkH9|zHrvirR2SlhU19u`VS zj~rVaM%(2JMl_Wi5Qd9RVhG0PJ*(hIl^KjPmRJAyCv^F*r(yZ84D`^sXC__#<7kln z(O^}5v$?=Ofg>%;fZu&LV*yNd|t zr?x;*b-FL|8NEe*b!U#x=psS~$#M_(cNv?cS`Sgqu*$@&WX$rxtmH1@PRUXzV)^K7 z{BopAY;=2d;qCQlFuv}`G(O4*K^Ge#Y?M(Kk&g&iFHIMG#l3nPmX8kz6RotE_6qB~ z|J1nf_U5NGPi*S=R4n6%MN+Xu@&(BzN|ZNp!)%0mRe*5&VXI3WpN3#75iAYCGWJWZ zt1*(!kIY76{u0cN?jk;xEc1}h`}hIfMQiMz?i^w&2BXY6z82xq5Uv)n)B{T%pq5(T zc%Dwr2J|S~^8vvxr6B^Ju~QayyV<~gDAc2pOB)G>K29 zLh+$ONk*aUl_oaez|{XWW5V)DCbMINOwrcEk?!L)$iT20_?;kV;WS#fHsGCt)C8c0 z+zN0Mga7 z9Bxn4%sX1r9{w#PvY9kf-T-c3yG7adfKe<}x7#L&C&L8s+&1Y#;|zn!@pUGv{y2p^ zTayKOpqDiUzAHw3s6{)_;qUK+7`o|$s{!`UWcb?-UJGnzYNp3SPs2IMW%LwWTKxG7 zd8c%crGihbYDv2hX0A)-=_02wVR4yZ1=`w^lP&dpZOaW5YFO@H3B&W?+bZ*NN`~tV z*mecARi>jT0eYX%d8u|d?Q=Zj)z;c>s>eJ7HT*LV*d|bnIoeyt5hIx=&UOrz` zp|g(7vA?aY$P77?q+eXXEOT~QX`>xHvGPTr9;)PPJAyT|o(cC!y9v`5S~%)1`r0In z(T*99LF zF4n#^TwVr$HsHVa4>$+x7cpZb$0r0x=1?f`=L4XRh+n&5hqxJa%&jFaWl;1u)v{uK3USh=fb>&4$J3h2hJ@A<(ty# zn+dUNoD%i$JshPI#L|xZCWFZHPWTD|{2?{7KZ1(K3HN$80|Tx!VhKfa%vAaBNQIY9`){~ z4n!^=FuJuZ4J_0AuJBI+u=dABOX6-`u_lhf*D}2_cB0MBIer_eE{(X^Z;uv&Hfk8Pd*VciK9k7_169*7@M>n1!jW4 z+aN_KWXsB#-E6rzQ`lnUiOHN8fd`XwyMH>0Q4$F_f80Jxn|cNWZb{(kHNG(CuVSL@ zTL(s$5oN&pCVopTJ_%@PZmEsbrRVyjB^tKe+M#PrhmSQud z*tSq?TPU{i3up$#w&h36?XyYxNN9{%M)9EjRf@@=$fIad`yiSd=MYV5AG!(11<~Ay zMKo7q=_VkL1vG=AiTzRBBz^kM$e;&2GO|(d?mU_E0oa zD4Ne-(@#oG>`7=8j)M_r7rv}=9o>^8_rX>{rBXrNq=HJNg1Sisb(0F}8Wq${DyUQ{ zsB0*wg_6--Dxv!nq)xO%AFWdKf~{+18BwZgDOKr|s#;1_Ev2f4QdLW-N~cuS$W(nr z1nJ+ak+a)*fq9kUeAP-nnr zTvxZ5YQ(ZSzUf0?{w>%c_#NmW&S4!k&7nKEe5790kJ7DS9S4J@&B9eMSMg8M3I2Wi z5tew;TQzDk4)ylenyc!dQI(Mq6_%+jJ4>$|2(N=g6?|@6(RI9Bi#kYri>9`&P6^$_ zQ_wT3tK%PGV*k6eU&mkJR&{&Z}@Ii0i;1^FfsK79|3 zpes=X2=@Zv9=(TClS*uom0&H>V}fe1Y7tnyyn(LY-FKbAy zZJiZ#Lte@N5r#uw8%|BqX||LBpK$ynnzd_y$UDJ!GFaee9fj|if~hLriLMorxfBvk zz!z|Xs^TqZXF!&L$Obll2^~dJNZb=v_<%i_i3iJg8=C8m_>LDL?Q1xsn-tQS1~btb zjQA9qeFL)-@5!nz=NV`Pp%81JKp7P1Hr5b`_7Z;l^Rf&`eUg?lnyIjzIM?xs6z(b_ zO9AIUDsFNlOXh2xF9iA;->K(Uzy@iMa9K5W9s?%_lQNaXLT0S$Vgf_fsQ47qFozO) zThw9swy1T1+XD@^MM1Pm4W|rq(_e7l07hNp{fmzS=)_)o5uTu~49B*?#$3m}-@-`u zHo`U9FK8B3SGvcs@;yK_1u(q}rXpX1>RVYy0p(6oR~L|qrf7a0UmoxtJ~99c@MT8T zk3i(m)?6`i-5jvecaeJ*j__qhuf??n$*-eiwku1jGsl>I$H-^ZNnNVo>OQ3%HA#C{v2&}9nJa;jK=a_9$#5cbO z_#NYNLsY(D0h5U_xr-c=r#2o;gmFm+P<8>GxMNWFJ`#T(fRQ-8d*Zi%mb)jO1)yWr z!V(p^%oG3HJE1>Cp@X9sE&F1A{uT3lq$w5KvAf=e#M^wNq=nyvWA6JM=V{1ClT8LW zyB$64rCXXHtPbB0m!g4>O0L((o?qh`s^f#8!#y-3&gZNqF_8{)gXU;SyG|H(nVy^$ zX|JDY&hSC8OEQWXxA=^eSz2iK+anlo#iwvlvR)16{i|SDOAMOY0H<%jt}E!ywxi`GQD8r0KZQRy%lt z9BE!@i8MiPcAtD=YGFIlNmuJKK6#Xua9!y=b<$XLYwLeKkWVAc3pCQicK#PPjV^SC z=1)gMKZxUB9L9e$j$SxdJW`aE*#IF)#!d5DZd=;fd=!D-Ah3kMZ~g`tewHo^KR|a5 z7}AN^`Va7QcBnKq*t`BgQtLN9f-Xx*4H=Z-K_1f-tp4l2Owrfq!6R=p;sl zdmU)c;cprd{U0A=(Hj9+_NLKc`m3dMi&hx7D7swR>1=TueE&C9`O$Int4c%oeXRt# zHY#C0`Ba#mup~1KKI7KP^AG5l$Au-OoYeoCTvi2I4Gik^h_o~40qOk|TWphV*@{IA ztTdzGKiISd*XtWvn|9FPX|!qS4(>P5i4K9~vpGJ9ZsIOqF7lD`CT`dUD-7{DaixQ= z;T8o1b|~Y$5ZsA^dm;ED8Jv$hh~`w@{nfhaDdU$NlOlvaMB$4N{)`O&@-Diq7ts(D z)rrIv9g-IO==gnC$KS;Q7iodJSfJWa!OsZ>FXTNibSyBykAtoJeIqPWVMGhvH^Nfa zj9Q8X908R8TMNimG;T&`{&*uJxh}rvi2oUjmeZ0yW5KHi_6IgUzR5us*`BuUWrv@n z7dU`*F~l{-ax(va_NSzhgtfI3*0UnBm+}l{{V`(8B)?~AX+;*b#E4K9R3B>UD zVsI`0Mq;JO!vF7L&IL0VzB$>s*rP1a&7wo=+TV#phJSPKXnN5ljeO;&?tfnSsm2No zK1geoWkI$4${r#PY+sPRE$f3wGmXo$Yooy^O6Ikf4dsV3u0Cr7)8igy~!mU z5M*6H`{?X`hvpH|Sv?v0DD*(u2t78a}IbYUS7D3)3Ow3tj8kfzoS= z$Myd)n%J^!OW9WhTM$~eY!NS6pp^@Jq#rAdAJ->cMCW&lzi?(}`2NuSt4cfk@3(G} zQz|}V=;_JTt8^}Aj2pJmTXX`wT4YA6N1SnG{OZ2qy=G^0?$F^}O&vEcluS$Z3iQif z^cB%n<^EvCQi{+Wlk$^ zzNePZSBT_$GhtMF(28e#PAy&Pv)qH;8A*NQpE*9`dnCe2Uh^g>ry`|d)ZbR(i7{@&vM zso+2?Sk2yl#Y|!?CTs z$lKj<;Y`i+1n6nFYOZCH{cLPeOJrG#XbczintK{9>Q%ivQ4~_eXGm_+gFP7W_>iaU z_aZ4#k3V(AE7M~4)yO4mae;{BjB_O+nfOnnDjdn7bfhZ&RojMo^ zM$b9^yor^k?c`1RLt;grJ+$Nf15)<@iZwE)1#L?kT>+ycin1PJL8Oz{lba?Wmim7{ zl0|Vew2J0y?k)~{hT~Q6;O-vogA{4H9LLE?I1FMU{|Mn6KA7Ra?3Yg!upGk&ie%Tz zp<*RR=SU34t6)yk>^s{x``D{v;T^;l0yTd03(VNOBgjdQLHp@ zQ@~U(qG_|59B|m`O&qH%!AQF4YYY$2ILCGqc@KCzPKXEzIUixTg4^9KWLbos;&e?{ zz)xkg^w6d;7-(P;ZJF2fnkxQwH!-itwlzF@B71(fUyCqamWbhm7?d{kl@*De8oT>Y z#8mzUCJ&av-mh6;)^kee7$g+)=1TD|xaG^_D}#x4sralC-c%}Jv?14u>_R_fHG1o} z${^TRk#iHy)+*6Y7Di62c>XZ!($6UuR!_O0-?4JXiWM62@w}lw=wP8l1n)%h^=SJT zn{(D-9UW&UE|2w&^6W@>^Uwv*>1vi$+~nXP9aCKAp2VP`?GJs)ze(3ueUaPtcR&IC zq3?8147;tDj~KR`c07ds=imjxwtB|(Lb7k#yHkc{PsqP!&0gYauisHmr`-PRQKy26 zHm&CiTMie3m8ht%Q*PwcL%15$QtTr(Cirg;PCe1T0{Z2_ka9)rRX9|qj0G=&biLUZ zJ_GKTC(sZ2fBy?no6T76K2LXUCUZJ2J}5(bFMC4wJ$iDB-Ri$}m7nJ7dI1izUDwZ@ zW}^LZZuVQf+KDsHY=Fac{FC80luP@@srV;dIrY(#KGL8{7*IK_a-Fa7<2smE$Ja>D z4aY>uJQe?-R8@91Iz9A6;QEs~zA~tk;U66GUbN3m+s(?Nn|`9|r3mxnE8SjIc2V&) zH&mq|=Tn1@2W~j7QwPe}pa14kNl{zNhd^r=a*)wnY?wlOk^~C`H!mMI_tVSIpAS71 zao+o+n_j)vzsiN4S@oAmnp4O7EW{y3>e{?h`0$4A=HFHH{_d0VjEp+UztWW!qlHGJ z9hXY0gcc{q*Wl!Is3aZW-)7XwVaT`}?E3?-!50~NFjl^yPQ|a5IfBu$6ZHP7a&;1? zfcEvu!6N>Wcd5xA*x^14;yy?@*yKN%`v9n*>9f|IC`t0U$J8?g^@Z4v28$SIPHOrW z27!kC(a%oMy~LZu*6kokT+vhG*r%K6Nre)Gev{DDVLYbNp2^KaO3{tj^f+!5|XFQa4|{I;~lg4D+;N9*|*O12#(JBE@SlY_81LT-F)ex5_@%oz?& zGp5Ho<>_PFc=()(iadGhOxUUAi(Hp4_taA{QNInvbQW>UMiggJiu2?J-lR47lm-wu%_#UT0Vk%da7YVuA?ub7~k zUdw50nfIa!l9qQGr4b{yHvWs$%$3Z94ECJZg8JF0InsBcu+c5oha1B&5NOM95wvK&iG8n&YRlZa3l24#)P&DoQ~ z?T)5fQ7~{6lGy*Bgs|vegw)z2Lcr(*?GrV$nrK>04Xx(7$-6$m*PPrTZ&ukXyT`J+ z!pxH0vbye@#qc%rTO8zS(SL2rERdd$YITsutEfKz(+KF)(p-kT$C!6Z&U@UIBMNB? zZLEjbR*&Ih&PgUZv*8Tw#y*HBfMNtw*ESoyOq(&^Q-R%#wI8;(-*=vv1=FhQsq z6B+D*3Bo-LAfHIP3>NY+wU7_dLejOVCL6F8n{4Qkk^Ds$j4C*q06#)sSjLfb9QRY` zLQzm%`#PGzmm3y}B3l=V$_)!eCw<_UyiBB!mx*Aj0gb#!gmu9j{H9Y;PG$UxR#b_H z|Jc!T6pAj{QGiQe^sT&>W`KB+_-9WHxmt4R5|IJmO$z{BB9h>OjNY8nCEN zNj)3rA^c&3wW!n?{uNSok@5Lkd#j;yCI1N8z!)JbCO$P*mpZm|T-o%tiyY_s&eM*W zmVQ;w-?#w}h1~Or85e?G9jDG-IC-INMS_2Pj7I9wJWp^wIRDhVi0n(Tx5DnU{gL%G zaWh{y(=vNfxqW5Zt2s4!cQiMwbB6YrWY*vM*z7Yib^N38ww5#cX*%5=3GHr_hNooe za-6cgQrEUsTOe6yDijxP!OmOhqKVZlm<*FMuh(4597!gV$yUQg>D3n7tE5FRQclxB^@56AdtU&s< zVYKO8m3onA0sS~d`c2q-Qwb*>I6(j9ixP@JDyvq&SoFWgwr^w)!OjTiige6`jT{+c zO}-5v8!4M5Qf!HF;&TyH{W$uF!i-WlTdqjB0sC$!!5=OP!yU5g8#3}sZ)Z#(pC8gs zvbXYHpd}V$zu*|1wWJC*bKkur#$5*tCTj9!^#u4RrywFLK~LAG~3UQ8NFuPzp)*9nC;-5|UL?}c{_nfL^yx3xujgQ@iT zp!9CENY8~zuf!m|C+gq z?ciySQ&7cisftJDYHQ=?jO|T~dXZ_QuU2xlF5G+rXW0hM z4({b-eOrS1CRL-pweoWBmNLD)^i?$()WXXX125yQk&lPWddEv5@^UL0dFlC%mH92K zlw(7*p}fpD@KVsiOSmIvD)TY~c^N`^IS6~tqcZh?*;JjzT9ZBjWJ_E10s}7@$jfK_ zxcTpRi8JuB&V?go{$Q*qhjz;HOkpvnCS}_|r2w%+SxJ5rN<=vyyX_YS80mX@Np|FU z!zI~ni|8fU(dSkw`*U{ORAhefq= zrA=&=G3cRP{4IV3^vBw-u&dxJ7?1;BVizjuVmiacdcH2ro{5P_ICVi=7HK!D6X`eY z6zQY)A>9S%FvCs7IzEM7R;=S!Eb^2I(~>!tq(DlWar&)zXve2OFcLVL4I*EW0kg=L zn(kGTURCGCq$TOBlNo1s2Om4Fxm$A1Z_uw`IrPz=gzkT#{f2?|lor~{q%m+<@gJk9 z0Y>m|6rkq)cjJGItmRagL55*8a14g+e8}`?cYe>%awfki3VdiAO80+LfG$rtOQgkV zaxSQSIQTAJ7`xDE>M+gJ5VwSgQzxS$y@Ho+*|L4x7F|JDR3=^4(X^BWbuJbM8%N<% zLTmwg7b1Nh32_I>*_o8EpjOCCt_85L76BJoxguavh16D&@`!8s1tz~BF<;7wxvw8_ zkWRiAqR$czZ8!&gT*8S+zGo~|Eihn``}!9#f%Fw(mcqXc7wh9c&W4Ni)$p;1{~re4 z1&T!JYh@yM3)0sG&0n)>XvtelnGBM6m9F{zkhfReUl1CnrcaWI={*|JxN(qaIsgM=Q z{0O`wnxAhq@NFFQx&gdyPAD^&Xy%eG#8g8}Gf0Su@so4k&p4iNt6@Q6!1-{ zGU98&2F5kLAmePH?H~J8v;Z_^LsOb@)cv(r(RteTwJ>u{Sh>DrZ+4ZYY-ObVVH{18 z4+otuuiBJdqA5Qbma99HHjf?dxVVdc^nqDs8vcc&pQG6}-E0;AsA^lVqr>?1vqo#W z#4WDXrKGWcsR2H@vi3gzg3@q7bt^XfmHz7boJtMVn`fJ8%#XPx`|AQ#_Wr9DO*Gpxdy>X{jps}hmlNsV^t<99 z+)^GS{V|Y$6HULt$;@XO<@#57O{jvPI(6j7lcB|ZYtXx z*{0c{P;$UV{8}0eDcGkH0*nfkppym*)px1~#Gjk~%aerFsa?F+zh&mK>xog<)2nTt zES&Zoqwe${#iIbD3YaFsjA6oMcC%Ckoi~Gm+e7n=ECYq3BIRE3^^TlQde&c?N;!zMkiG>h1LUi$sXaZBR;|4-*=imJMS2H;)z~rO|dQ(RGARs_8U#wBC?4AN#L0Y-(*1>iVl7#hd$*Phbv((h(bB2 zuCjF%$@gxX#|+jJ9a~Q33aI(2qG(_18Lr-w^`znCD(uiAT#1D*&sRnj#3(`%t|wl) zP??|vXSgD~ZoEZLW)Hm8xy!&|^rFSaVXySDZ3ay&|Mi!$%7%vW@!x%GZazpqrbI}* zoXAFxWl~w&dgj#rpF%=4>CQp(btBotZbIc*RrsN`zROqnd+60Sq`uYgS=G#3JX&oPuPYqo`+W*I-xmnEI1q39+s(+Y2u$3 ziw2U^kU+lAgC6+t9<~92QUV#h%so$JhF_;BxA@)3JyW- zYtnzMJrf+@eq6VN^kWvdF7{ihUAQ=`40XrLI|tfP4g&Jw%N*mZBH$`ua3%~jaF8qz zC+N}{ObW9PpUc!OOJ$Zh*#y{VNjLIwJxl-x=nWbe1Opp;pvC@d;)K~YYol%j>l0uc zGoAI9$zSR5lbe<>oKtpgJR$lM<#G0DnuX z=9)fP!IA6m2lN%;k1SFC*@u((EUuZCOn+4*KTsx1-npV0KPQ3<2RDSGQ!yDunsndO z5uBs9r^D*D$Pn>BhKR>F?DI39BNN0yW`a1pz@Kqq>)VH+^4o`2Qzlv(-ae!SBSg}f zK7qHWoI6aPzzaSbk)|o17C)XoCE}`!cV*6=I(wYHi0AFJ(7}DYeu2vHj)& z^bO9L3ntAE^@>`Cfup03v*TQifxfq?)!Zt<+9NV8?G{%J-@s>6uJ#^1eVWPW3v+J9 zMjj12tN&)B?GtQ15|7PC;^haiYUzVmKcA?l4`PKd(}uJ@h&7x(h$Vye;Yg5>ww%ek z9Z?I62HLpS&ZyJbEMu}poEY3!)7N+KoZ&ibZ{q$~ZhkUc$Pw|=0h^y(7m7~CWE5-C ze8VC*2X8NjRkZp2vGx7`r#3?gDnfgz2HRhV?WrRBM~G!qjbGmpq33^Tu|GCe8P#Eb zg9^t}4TgJEgWFU6^*5;Rt7$R_RbH4um3KHYKb)%WaAa*7Roo2*y^WXEb{i$T?cZuG z>_fU?pRBbQWZH;uTvl5+{sMpgm)cHleX^65j1ZyW1GNq2-s!0$%n(sK>C26Xn8iNZ z|G#t->;9CZC{Q;I59>5cP4njDCkAP*K|P`He^Jk4%a)b`f%f4^|R$Q*@I$==AuDAEf>7kPX}v_fk4^<(mbl5sp+@4%g`OvQ?AJhI?AA+ zXWl6&Os4i_vXBz;p@RPP#kS!EL#ToxXde!o1ROu}>fS5pZJo{X|EZvG$e^H5Br7N! zr}n9rte~`|Wd)U6dM>w6+|148=$oop;GeC?E`>p5Fo{cN_eNr)c$|q^6Aqh2r$xSA zi}kw-88Eg~l{V`hiDVxw5s;+_zo-NYI3#379zPzU_e^9~ukcyxs-3nl~LhAb#OO z+_4i8dhd8<^|Gbw+_jSz##aRDVKKYwryYBbYRcRiz8N~syfZPnJn5dU2h0%)_eVvi zYYUIhwL=1l6q5l)nToPfC3rM>3;S2D+_hXg&fRK)e!gnn7REM(gq1>ARfburD(}31ntwEQ;frN?2Djd{;FdBO+Y=k3BiYsoj9rsJQ}q9kT!=+ z2wCspzHrIZX|Bg3^|RIs({mT!dR>-PlIIjULdU;0vSjD4IdtOK{)2}O=rQ%dZwPsW8$2gvJKebXHsPgmo^YPMn9rg=ahtUewy4Y`( z_fif27~^sDk{p(vy^wPDbbL&>r=8v6rOwFcEKqS_+RQ^Rl=H|kv3K-Z>Y>|JA-A=< zS=XeyX`IwBs+7-ZXwt#A9DmZS`CmdQ)L63WW-X|*B+r}17bw7`5Ks%1VaZqY@Y<4q z%3*_#``V2=cWuzEwPrT`ba?A2E#G)iI*mxw#2dA8fFc_@b9HQf#UmX4FR|!QO@lVj zff>h=FUbHV5K3qz?NL)uGnLu!NSG7ko*r~?*X{$l^{LF(P4r~Ox=s6n^-@k6Ob|+6vw@||vNO?Z&S8&n za9O_0Q@@4X7>fl;$#(29SR@bYfLg6JlYj%^{1~JTQF)I0JGmB1N=S*yjjtmM7 z)o+=?jJ!;B{pyv33#p-J)-MawlOZZEPj7#!>sO_i0!8*k?5G#Pp3ORSKKQKm(2*@$ z_v;VmF}pXc*|kdR;=bJ8ixN;Evl9-7(qVJDBDx3^#meK5CwQMv@lDrWO-(8&o)c^v ztatj!^EE3pLCnb}BJ-@3ld6e>@dT8PuB5a6Aj_D;38iYvmgRyv8QO z;FAxSo$Sdz$z(87!`{VmQ&@Xq<^vddbFlWT1r>}L`;5@11~x&5EG(F>jD;z} zS}P{-@R6;@v{lg=m7#iQ2ak~G*<{2z_jQZcyLAj?*F^?ItUG4`BRW{H@Flsr(U)=b z_x5nnZrHeE_j>H6A(SJ}OyiTVo2&vKzVRdy1iQ0{3CmxCVGzJX6CW$+t|G(9dPWVs zI2hB^QOIDY{KA;Cch++>rGuPuou+JB&&fvfNy5tv#pyg4TB3yh*c5f;6|de-DgEBw zWz~ws`jCapPobv|o!1seESR)QPZqOVR&U$1Ml(Kg;7i@MOkvvc*!KFxZW4NI~WQDrc=L|F?q!ZLeiS#XJ5O_WJ#c)n?l&;Tevn*I9)BS%^sL+ux~i5grSE*Baf0KL zt87GY$iT+(lSZY&;8aCoCiKiy!cb|HP+6FHqjqZM`0-P%hW}V-RjE&HW9J@~mJtzk z;X;J#TpM>+wDlICDS?wEirnk4?z-~wE1~KV;}sFVHc=a&aO8A^zG~cs6!UT>B;sg% zswQ2SQT1Olbz)yK8_q|Qff`Gv1*0a-36HhcIZjJ*D03^@)R7Mi z=w7Vi7Y4jg{d%Oh;#Y7Z=NCd(O<{aoNp6IXbF7Xod%&W6*m%h}7r05c-LCO3z@%E( zt>S-9NIaE%S#x4*@Rnd5bjxS?N2DL?pX48(^pKl>bitANdOvo2ATt|5pR>FDcKNN) zm^hl5IqCQpR#N9`x}*XNtkcWW;Hs}NEUe>KgftloFo;}bNGBT@Xif%N!$6i#A${vq z{0is`gVM>Mau}3GzE;6txXKJ-N05=eb}Q_b+hA|-Nn3Hj_reGm*?}%)!i4n746v+F z+-_7#li&-%*44#pu6D?*DqvRX!8M&MVYeOLe&mSe;P!(%4(Q-e2HD4g>R8_8d)lN( zZsYsuYpZmZk z+N*idg%?mYE?2>mDn&^hw98h)Pw=&15@c8Uv*uoE>D3?|^dTvNhqsIG{4H(ai7I;Q zxm9Pp6S6zFpPL^xbywS0vpcB>v#Q@Tt3eELDMw0`K_#4s!dW+_x11lCESfc&P)|7Nyt=}}A@nXjXGE?rVyvb6gYrwLZ zmRi46yY{WpueM~?9}C+Qp#^0^J*bcB!(bf)9WN4NcHc#&YJX^HvG#P(hD}G27yn9I zpHn{4!-|c*A-={XjqpjFTO7aN2$H2*VaI_3I}U0O9oV|{px(0iq|YwDeJc-lTof%V zO7p9}uYoUOf3Awsfm!nu!F|CZ@A;dHrClA4t~ ze^-X@XSB;7<*MDdVf(g?`oqr{%;{z1|20F@OMWCA8K)tg(>vd=((&B_R*^56IQ9rI z!5fcnKC!hUKQ6$(JEk0dTD3bnIw`lKth|FWiZWp8CF2}0C1YAt_)Uunzm!#Y=k}dD zHP9)8}3X#y{yS* z#;Inq3jcAeAsfqK<3m*g*~m1&#w*EVidlK~WlI6H%?G0bMSU$yfKEyn2)P1|eiKw9 z*UwtmUkj0K?q)9@aZpbtfDsEd#7MAY;S>M9Ddb}<#%Dv=(VLk$SDOda!j>czw7s7X zA0O72BJ%ASB}K5axH>~obroh`rZ13^3_@5q2%`iiH}_SpT1)4Qy2{k7{MZoxC6W5e z;3-s&9Z$N=g>J6&@{V(hJc8zG2Mr%bJ|gOp@sHEuLc-7CaK6``DU?RBH}^0XR4q(g zPb^BjC^IqfCh{N+6pE0A9qV=k_-od0Tfc3Cj!YmQ3)f+!%F4&ub1dbnD$Q@+DLol+ z5!td3Oxe3jhedXyJbl8B4rH9JPEjFi&R3g1m05dPmwZ1=e>u^T-LZfBfdiVu+YW6z zsJqA-jP8UASa1pUmYc(1ts|%J>Bujo2HCfpinL!o1Sy!LAv5lfPr!JUj=%5?8ere&s@%O9r{faZ zLJ0DFkaoEHm0MamT*qvF5}Yp(1_oI_gYKxQX^-!m)IqQ2xxx}hcW;M{ZFwgtR23U| zc6q273t_Zkf_OI4Vit9di9|Bv!AO<_NPV0XSS6E zyr&YljE1VY8OD%b$3Kx~qZw}gjOE{$RjcmhFPn5!Px{Mx4y#ndHcfTxr8OOYgFaoe zhxvsiQ{c#*6U7C4pLeXtFk|^A>r*{q9Z$4{uTgmZH>FFy3Q#q`tSd^et`HyqE%#j} zTSW#BV6NcrJ4}xJ8~P3+)~fI4;&AaWof)J0-kO<3e~~a1*aSk)+b}{Cv^8jRh;FPO zvuDNjwE>#1J5BGmMW@~=(*GdHKMUNlQt>b7#zDVQ7$rxU`^ItC$YlZkY#PqYV9B3< z3}+~xGLD4=JVN~h0i*oq`8R9B9K8};-B98k-^82|{ibSdH z9N~V;Yq~|E8a%f{PtwT;+S2E@!9spqRvJ>suLT$Mwg&stQbyER0r=&V!{rvB#VkspRu3Q)F&SkrmX=^Sb{)LfthxB}N!trzTjpM0?iOEH} zI9KM(sk0~OgpZdK68Srwlbq-5HQzv^8ji{pq~(QYflDs3cv1Q{t>tMBomz6&#kWo^ zrG&qqT7re73JHiq0>ZQ0-Z3~23HUSdIPynp4Uua-BiH(rN@kvmmwjukTjXgnSiX4rPYzI{!(8}@~wPhI9x^-AvpdIljy&nE;Hce z|1^P-ge*kF4oDeeF_6Z_Bp!o9>0BD`s-T-%mRfHc1kt^ zIErqe%OmtNNRdE);J1$ShaPezQ|Z1#1x$f107#V-4sy` zGGWpvGMui0I^QIkOlG$VG@0Oh6H}ULMhmR;S%XqqC@F;IQ81h_Re|#e&|r9{A{r~= zJOWcBnt{V=sf@QhRs&nwAnxxdZl|k6dx7HCUI3@72Hf8npebe<91&Dg^xskRPNP9< zN6~BTh|_2TdP4!&+$wb7QR-t2QlDl{rWvG8%t?Vk*wXzQiel+gWiiAGSJ-ke&4i8^ z#b%I><;0Y^!jf;L_RMHjjp8XO7mC^Iz%y{X6vOZb@GS%S(k^n+WOgLs8FJhbR&L>ZA^|4Y^K_YyvT`99+k z622i`o1c00v zeK026d~)-tjVIfx&u_1mw%<{NOX14!_ELBzOO{G+nO-be()^bB<)rW%<$KT>%Y0f|D4<&)?=W1bwttRN6<+FJ!_uB7x4M@ZwL;< zlY5VtqT_E&hxJvYBSsfj3-U`JLI(}ZLAUsGVuJs%M(ox|_L~=~zY0nA0Z3c`Ou7e3= zzOc;O!)JkZ?!xFt`e+rXUtPtZ;0w+3nRz1zTUe6sb>xJ~!)J-_V&BNw9fA)B9}YU) zmN0uSPp#J$UN|2Sg=6DJ{}WIW3`HjtsnA}U1^=SnmXQ#+H*oKM4Opml;{S9~HM?{B zZj7!bk-q4u%)$TqqNpa3Nr3;t9PA?5w<>hAS+nUw?0(P=7!g-GQy&OE!q%-jck5xi zYX9#2yZ7pf^RLI<(bipeo|>=UyJ`1^4H{yh3f#IiP^T_}B~^+rDN`8+y95X$!AYsa z7A7;&#H!|rEa1=isRWZ5b&;2pp@6ehN+^~>1rkh7r_P7TV9PX5tdb_GU^1~yOCrbN zG-E&-FJ&mDd$3O+rytIVaRo9BWZnak4T&mdB97^3K`=4S$Y1z!>RrxW=K3$?Ay zxQ5**U6Ghm4XWyFCF;dIA>z#Gm>7>xcXv;3XXh}l2tE0%&9k~{V1OA?-MzZ!GApY}tOg96WMzik(*o2nFyx9NHxBw0sLbD$J`3&Pb!$^VN+?=ap{zzK-#D* zI1YNz@KvNlwNeX!j{Zd)iklg7jOK+)8w^-t!!g8#Bhor%XY;A8D)k}B*??b~COHeF zKdB`BppxmFFTl~}^^6R=p28w1tsoey3&uEET2GELJEc=w+2-}+2xG{U<`yfVD?|%W z3jG*zMO7>f62kzuc2lt#2dH|ZjU=5T~x%=Rb3XwuW3bZZ;PdNj;2)?D|_|DjB06|?TZx-)I9 znxojg>%I1RYI~bcpQKmYue&70UsB{tJ(S?NPN-o~OCAyYL-+d(A&(gB5}|?xbTkKD zIfxfQgDq4_H04AzCpt^gQQo*Y4h|@8zpj^eYs$YNvR;Jm$N?djjfOryJ!gip|Fprg zxwjM_15AwKmo#O#6fa!a8XsGzDR77#vsLG;N)2AN$klI|tzO;ybqm~yG$nl5=4!VoY)1+{is5o7wJS&9@h3sCK$c#lUVx*@9ies=Q=R0@!%)b(=*tM=_`n#QoXR2 z^P{gVsB;t$jS=z^6QT8Dm9QLUCab2m@hD(^$%l9F_&?*2d!j`4cT(&T8GN5_X79yllIQ)t??-+Hrq7u( zr_9crf96%46OZXCzo*TNA&=Y*?`bnL1wuyD4z?;#JLx44TQ8IN6S_q>(G0dLkeP0g z9YaWq)G}$MnWLSJgR`3o^;Rq5Ptw$5#mpc{71IJ#W1SSqX*bQSvFbk%DVXunX;^^HOc;j4953@?S{HjyoE^q9_-A4~s)zMw8*lM24z#~J>R8@I)cgR#z@ zpDZ`sw1z>}V8V~2@2|oAsid6y#`Zb5jX-`@P>8FumFv-vw78>D$CJVihHu_NZg78p z{76F9QB##p(`39aTd_#W<}W8DoXxk&U7Fw*8nGfED0WrMjvd>!@6c{pv^2zPsi{h` z$u_=IrPwOj@Qpu|rrxqjSQEGEsCLF;>W5XkSFW(~SiE%hdaY`)ATJd1FzGH}!@&ms z!yk){OFkAByO^v{U`%L=D6bJt!)pWqLSSs@F|0-XPtS+p>Ye4>ya3Emv9APf9(XZP zLX<#uz~pO+$p-#UOg|76tqskl)~$ZUAfGG7lgkymcksT!p3bwa^k_U;iKow`;x;`# z@D?7>2i`)XN(cvTQ$RaJ-DRW)B$f*5@WdLn4SB*n9Vk5rX0W9w$4uFlkG5f+bt8xy za*^%UPq-PFb>FlAZ9L6Fw!wl5x03x9Xn}-(D52lJrH0caZb~rWL4uosvmA5{&|m0B zjMm7HYWUz9HT08~m?=|8%#_og)*PsF3vPkpXh>#SY6j@aTY{yep$>dfOiW@@_(KMb z-v3eE1%H0a_cK6r$N(KN(aA~SGC;>@OHd2p5kY}55lPqtUB|#{E;ddu1hkt+0@~^9 zW$Z0v0sq6rKGE$b0A2G809~;LfG$$8hE&8WHn5bpfleP)tH6nrjI#SgM(c{zqjgC& zVZDl~`zrF~*rRqPL zU|++$d)%KetBeO4dgY^DuR*BjrcFeo$1cC z8WrZ>l(S?D?QO5;VQyYV3SXCX_WT}P$bVf}sFTJiEbhcQ{FnWnEVnw1>KKd=Dq(vm z_*SYPH|>C}yBpmaIUQ&P?$20qoER*C22FJTID~v-<0ja8_yJegXjw^fjcWz20XCu_ zcOkk~Mqhc{bPTp}uY<@--ap^(yUToAD}0EAyBudpO9k#7RFDv_1dsQ;3}d?jwsXxr z80ZAwVVTz-vj-ZCg6tR)w7vB0hp_GMKNrwgX(|}YIE1(=UgIj*ev_=Soi3WMqOlLA zAmMWdk=i8mC#h`+{`qGo<|io_S4JZtf_Yr>*Gmuh@_$yey!o(*1hurSLVn)W_diBM zNDw#M)kbxk11yp3`0tyrt+~s3AKFR9(kb*0^fIu3F@^Az)syj+$Y|V@g>&7=!>?26 zXX4IjYO#sBjXXlEP>-iII)GiJ@!8rcXj@f#P8el> zAFuP2^YyiN&JRaZ(0BHOM~mQYl9rs1lM|TWyD%`s-90(vB>DbbCZ(0(ny{}{0dK&H zpS8p%$X!EPm{zbedw05cw3NRPpOKiOxpRIPnl^Wn#|9q}G16QfS&YOg)ff5JAr`L@ zR|hQkg6~LBB2H9=Mpl6v9@ngDO=_L~t`n zJBG)#IKkI1RUOD;U>I219+8+qNs7Hdc7cn{hUyq%%IqK=|!2QyB9c>?qUPyP#G z;kHtkd!dQb$a8im5%q&&xMO;Oq?AwP*lwdWsHj-3}93nw^c~(D+#LK z=7);$f@-=TLtA6-uXSt~gp|YJDsKToV17B84})QI6>6YW*#fQ%@wJLC8P!Q#Eb=CJ z%Yd=ln-p6J=msD6wMD|`)cFO47H zftq!UDsdZpF2K$@&iN04^B*Q~Ma~;k|lJs|O3Mq(zVUjmsl8g=nTa#a;}?`#SpGuh7#9JQ>upp*ysR&<;+! zF$0V==_hvWJu0fa@Sr@e!W4N>{`JHmb6CQ_brj1ZEd)^G7}Y>nAsF9m0>}G`Zj7o= zY?x$UWem1WioJsBl8hj>tq-ORwXH$=Hr#ldG#mR~md|MzCq+?%X=@Z4F@2R|`1FpO z=!mN;3nQ)$g4hn<(5gy79j`OzGJh1f>fVCtb4=wZX_z9)@+bN>SR6_|loXZPaVLGm zFDmFzU@fxf@H34D!eQtEKTu!Olkoa1iCnV6A2++~L}dJ`_z9rufUUV6SX!x|3u6pJ zn-skS)gZiN=2gbIQU`lRHBPvNEkDe1SsmK4cYH_Msn`AHhA&~@kM3fJkL zSkPhza6SHYt)awS?C3orPBmxpd=8 z<5w^4yi~v~=>@-m;J<-2v$y#(IrACzs9RyL5B}5*fXNJUZT?D5znxFUa8);pKP~Le zv~AEdWL&(sM?@~oYvufk%s1G*{yYzRVz5f*#V5u^`1yt|4Gs)DnwAU~srkghX79vhX|$X&y5?~v8M6+fSC zEOFSbeWT-`w-C1`$SN#+4K`*wh5t+8^lc0pjx2bb4C4euH?R0;E_jlL>HN#uRlm0( zQ@nk)=pdoQFS;~rvyLG)>-B>?_z82vj-Om7y3w}<Cb#nq6Zt0tw0oa3g6U5} zVT~oLS6iVVB6PC=*63FlBA{t_L~B*h zYg^@=(hT^DnNp^PXZNwRod<6GxknWDBBeOxa>@nM9cUMQ9|=5+!4@x?rNl*gKzJp> zs^%1K`ypKmohTTMI>GHqTD4-^#k$kCieczQH5lL5OYlFz?jm0Y>eEPl>#_3Z!bMXj z%w9ZD>@W4=?_n#sF=xqaTQ9%<;s|0S&t8g+r4eEl7yPy$h;NxDP zjwGl_*=yop72~~f&U)<+&xPny*LIytI(??2YStmWVv-nXhBNcwPZ$dihX{ZDSs~3Z zxT;u_Rgtj14_*U}Gd97~2w{V))Cd!|=rw4qGe0D(7wVIl4U2_W2Ay{vj2lUA9!hXy zHegDSbWDcju=UVGyfX#v1q??v?t-5%UC=j&RU`DjRr<-_S6~Vr>PIQR-`f1i->?~` z7(gzkV>@CHJ(4!2R)48cusg5ODplMJwT?cSX{QKT&RIhO^8c75fbO;7aZ#En;5_h!V?W3>ijh3e_Uk3dmp|# z9ypOrm@bNPlGwe12c-}k>9zyL3KPn1|Va zb-<<-%fs<7jbM(Tr+|JL^h6*l1KRglm=J^HAb2u?eksRR1ni`}LcCmT4JSP3^Ume% z7I##l-FV-6lpzniX*X4(dRj%6OYejD$v%hxX4qa`k3As050NcgBW{MlK!WKq=Vl9~ z`G2MDg&(mudhsIhMo$ZSGO$p>CG>oZU1B=NPzv+%Y8m>s8v0&?>GGBvU4Bx|U4pLs zEgJdXI==g1k7Nr?m(|4&@?m>$E%)b_1X8vG02yf|!FPXNlHg0D|@wp7F^=!Uw~! z28?RtocS^jo|FM@3^g$ww-E1kOB1Kt!eBdql=c-=4vqclEgY9TtlVovQsoKgU+~TY0 z0a%~)rtx1eTHd`|nZ>ta7`>hwPV}5?#lFPkS(RJTRFccx0upfR&0RiUCX6fXy8#pC zo-?M)hQH|YDpMf!-n>+Adbx>2d3;K$y>o5E<_e6lJQf?10xm*8On8bYcL2)E220r* zZ+epQD;TNo0QFUqzUu4j2Cx#g68PjP1_375^UlX~3oYYb<3fijc_|TDuTn<|x?(uq?+WK1^yZGlX4tsg(!txaoV? z%DrAnz;5opXWa#Z6QJjt8U}3SWgw~W5PW))8Xy#Kxey{?KP(EF@`s?$_7Z9aO-`dw zJF+dt8{CK|)Fc_sOhqzGRDjV}CG!|}ED^))(5?Z{U-@y2{bquT|tCP4bS9nT9Odaur zT(Ktu9~D@-%o`P>5lO3>9WYfsPO_1X46w_nCR!|1(fJmCmFxO_y2d8|pPk@V#C z0t|q#Pli>k>tI-ml*Pi2wIt&$7`>I7I$|Z<+n-VVkxcxk9hz?w5Hq$ z_oB1#M0!fp>%Awk`Om!|XX1RBzwq~R+&!Rl4tfPk)G<-8`eNs^NvSI(jt{v%ak*wt zN_XoSv|@-MUskNe4-<1>QcfanYc(F9g>?Ko<{jzqSG{s|r)3}*S-HMijg_l@PO`@N zFfEzt@EO*x!-W*`5J6geU0vJ=x0|?JX{=<;fxLm({*JleuDXvKBf$<-Jm7e@mV8u#cXmL0@$5hK7q=#Hg4l=`Pyo6DTe>Bj8d6 z<&%9O;+h6Jzy1wI+zBuB$rV*FZX4F+vl6Ytdfc`Q*kc*9Wmo+d&roLk&q9N!;M2F3cyX@amax)P<3kU2RzQ`2;B?MrvpfGMlgd zmtOwFy)@%q{@fv8hQ>7-j3g~~mPwFGfxB;n85wi2p1BE1OV{^S|D~n6)wrpL%HivW3Mri&r+6}G$dfM6eUsKFAjreb zU`(A%$hnXp{*iaJ5k9vYTj}KG5H2=f)kxrQC?cTp^B(fp(XSgkU(m~f-|H(@> zy+F%I{R!z2hs3{~zu-K$gS~c}f8+pkK#RY4VeR$T=}qghjCuvE9muDwk0-*-YaU}Q zZN^o`JM!b0N3_Zd8#XPaOTf4Qm(KdqmPw6wWtPPGiO57c_iv?T8-E2hM8=Ui+%hAn zhkQ`MyYNx0TkJ;EoV?rEY&+R|44o%`C-ZVkHTxDdV??;M+gH6cBp=Mgi=sxo_>?`)6oYmm*!trKe-QQA8-**Ebl`l z{cB<2{Y&YKeWJC9LiBDIEWx1dvNW%`R!;s4?G`LNc*0LR&yiYq@hqkprtZ_EDYlT6$uwy%F|T8Vw=HTB%vuEe)w(r~-jXv67SVEguZ z?j25kdJh}bmxyI+=*|5$U!VE0EA0}7UFl2AgUmOXxaP6kXZfl19Ku!q1Mb)}9>Qdr z1cn|>OO1<8PK=H4^9@@XEUG@4K;K*Z{SOvEe{2DKlAHX&@yCOHd@&z)vPS;K|9C?^ zC#}XEy*0WA;n={;C!hL%H_Ak|frKD$9;adZj+AaBMRi>T=nd|FeXipEK;~%Ge<_Nc zD5f!xSq7^<>ii$=4TWmHzu~(*_eVNv$0tQcCnZN54GM}_8WMO1oHn6?H~1un8U6V}yg_NmH7`~GSFe!P zOHurAbcsf+{eto1=VSq^g-h1xq7n^loC$5fQ%E{_kle<|rPj(3p_hb2ok&a0DNYpc zWEAH?Co5HEiXfA_KBr0{#p>w(0`w82VY*p-Z+gIbp_&H2bU6*4KNDaF{vFr&YpA3e z9FZFSHd=_^B^UgJGjpoACq|M6>t2~7?-)6^992JCt>@LBM(}%A?OM6Q%GGDN?M5wk zceVe&&Tiz7Gl2p2K?#i1LbUSAfufY_ql8Qt&5+^be#!gE@ExJ4CuFVQFTx7l!u~>~ zZ1$4_|J)z3U({QG3wV?LBNcCY;wJpF^bX>gR|_<|k=^l1QxVE@VUAdZBlCdl3*<=}R80iO0J^u0b3F=qUplko?y< zQnv^Jb=CW(n6Zs|88Wh)yGz;I1YL^@#qTmPkk2uS&31w7Jq!o&W1YO=1A!JXFUki#^v2g;trV9&!Y_aNC3 zG{@r235;;6C;9;!wSI!nHW)Xh4D8FMz*oda4Q%3$JhsHD)*noMQ9|3FipW=}Z%$!Z z-J9l)JhWCeq31lU4DE`O0pHaLBH~ge(;X+iaUGZ#I?98)6w+u*+GuJ9u8TK=e!C` zOHY@bskAD0&mTX`e#Qi^Lmu%STJGsApE>RmtcFy5``h&k2^l%syhUfjv)7oaY>B?T z*a*v-u$qeGp=}+u)nUZQUr}4sc2svX0*$Eq0@{d`CX<{hp%ZjIckOyr{xsAXb#k6e zu0FeBbvNRvG4yLxKtIbyDQXJ6aH-cSyS1kFzbjRrZ>y2CmBvs~Ph$L<)S&LPx7U`< z0U~g~ynOc3>gP$NpHG+-;4oL4=SrofW+xYDs!w{&9NM!F?z+3E>K0iEQ?A1Bay4{; z&tsaVG;#0ZOZoYy$`<~l0i%bPez{?nYZE2zP;%0>xSZ2htsagk>^jjd(9>0$<3OdQ z=fvb`u4F87a`5m(tjN8au7uCdE|_WW?DO3O*JG(ZqH6dyaKQ|-`kor{>aa>~11>zM zVMz3`{fCbvcicg54cOuuV1+s=p^xQAsOdwa4XB>7gMCUa6KU&|xDNhT)e8@GQ3?IrZ;t)onuxcB2Kty5p#MyScqLMxPV*7F2F)^6jgPz{4ZotkaL4I3%(`KB zhkFD(X5MpI>1PwP_XDnnOtqTh>O0A1&Z!F|QguJ*#!rZ!o_yKrT-NdPrI}uHeL{T} z1dvG8%Ta%RXybHpEv@$2y&9(s`(s+Xbaj~tXL2uDHGEg{Menh8)^=H*X9HmvV;5xW zYGb7u8F?MLT!95;#szf{d{+TWVLw07=Tt@c+3eyQXPkQM6J5Mqm->(>gTB0f>bc+w z4VeDk2=ig|fO|;Ud-$ZT$f(lpWkFHGsdN$b-t?{A2+acT!|^0ITRN;OGtLGFMK-MC z!J!N}&??joHeJSvO*nBA4^5X*6RpB5OA@6wa-#I+a`PMd)BD(tlC}Wc3yrr*trWmW z)A{JqsQg3uhYC7=PakqVZ9*na_@UNBCrP^3zsJnGam1X^7_ z8-Mzw)+dWv6y(2bzGj$1<-4~RDjQBFuUL{Gre{&dgWMDBG)OUUAQEvKUy=AH_X~~j z=dFWQ>gF&ghBjEr96r@MSS@#iS~( z7j+&v!IxlNjI1$X?TXP}JpE;o(|OPuij=`j7;+vB0WH`UqkS~#98bWq7`{G_=K%hf z@ifBlG#*%k@=8JcvEy2pin`5t)JeHwdBEC5ntqOzzrVd$`QS|Qvd~yD<202N=#ex- zgP3my$#YRl-seeIbM(Erz#QaKCXJthY|tFAnL?|YEg)JNZx{{Eph@7H{TGbEtxRzg z8hMwbQ^A)tk1|W^M7S^o6)3-5670GtBq}XJt3qQwstcddH}8WQ1(Ntm{UH`U18Zsm zjmDw{)D~Dk8ji#SGLUBHc{D?6hZD!ZwFX#OXMDK?_Flm`B@jwyZM?8KOZz>Y(W4Rh zQ)%=)@K1k1?VvX=*l4%F3WZ*g#*bAFLrWG7q&Cz353OMdc~XdYh;D$SdeW3jiQJSw zoxAzm=Cin|`~lDt9ib|u@iUP(?Yz@r$2{$3+T}6o2~pH!T)8!&`!6!lzpqe05Z-=Z za`#6NK|A6;0dyT2yGmG~>MS3Axkf(o+7dzR@n7 zaHF`B-?dPQ;`z;yD>sL0Ts#kFgo!^Nz19e3Rv>&BmKIr5SU+2-ot|{ry-{;4Vb`vM zxb0EW%tGibk7rb=0@~<4<7?>3^l&?SpPPdOSH5@3T(L z^9om&=!KU0s-Ln;=d}rhE^19X@51 zk&=*~ePVgYF>M+NW$P5@pcyjVanh8$fHM(d*)BG^=%+pyF z8^f1)Y+vHtUn-elwnw#dpuN15m<2DD!rZHhOOhRrKCf_i{`2)B_*@I$Pe;3yUFZ4D zp5>nUMhm-0glu1kE;HT&ZxwhSMe{dl&|=B+9X$z!uW(%JzTC0nW_o$ynw%AdHW1zc zul7QSu7HgD#s{U%ir;XJTKXF7*ps#=bx#@=V|#pe`~BcshIVwgLqEn1O{d*Z#9Ej2 z&g)z{ZlPD_t<7JZHv=L%%%*3ch}F((UDmqb4%X$Z!#VSC`nR+^BD^zxz|vJ8&FLyo z`(qEp9Z15u_E6A)(EVXo5!C_j6b`c*3*OWu+OG4O@U=@LgK%4`lGYqu6Epi=2OAm~ z=vxU}plF$J~LgtrRucUb z*^&-R+dM=hxR2DFjT57BVly61Hj1WI-^*!IsGKH+W;ch@D(U6r(zp5t4j*3_?}dUt zdL}`>Vv>uaXgloYs#|W4a}V@~X{yKr(y{|F2hJWSKCm1AOQS^xl-gJiw>GV}wPCB6 zMz*R=o5r1O+De!CT+VoYD)q_55H?-%QL+c$3de#+bO8%s25jS#NU4LG1IJJIuLKYAm5h>oF?l_p9HSpR`>SwyrbdBi=(;8E$HQl;*>*1|OxAtnC*7_Gl#aJ+N84t#viC|VU z8=0+4DQn7p$qr>h*tP8UY&@IA-eg~}zqV=L=9@Od+bnLgtj)GIhuc)NsczHQ=2@H9 zZB5#mx7D=m-F8%4o3_DiH?-Z+HmYrM+l;n3ZS&ifwY}5!QQN0&-?jx6qcT$sQrW0% zRW2%TmA@)TwNw?O%2wS~JyE@Er)t-)-Jo`p+s$d`-flrVzjkZeZEBa?uB2UQyT*18 z+r4P_w%wm*9nHF#^)$0Kn_xD}%+YL~*#fgQW?RhS&5F&gm{pnGGJ9b5*zAoNnEl6` zHfPPv&3l>KnA@4pHxDvjZobz%)%=Y4IrEF=rRLSbvSD^&|B!Y8}^x`;6~#XgH9i!_T<7UwO>EN)peSv;|LW$}jv;*EHkZ^Ntk_WWmjPu`jz!H?%>@eaHP z@5e9USMVG79sFKCijU=!`P2Ma{xV;~H}LoQC;UtP9gi$qS+=%RSz1|kwd`*>!g8|Z zY)fZLFUtVSFw0ez8!dNO?zN1vjI~U%JYkt(nQvKQS#8;1`NZ;#Wpn#h?Un7@v^R$i z#41yi-nn}^bEQ|d7VS~G&i4#;)eM?j1E%7jtvni350OG~YG#&p2hGBF%(dO(+f zGX?8t)t;GD6%^6X*0CP3BPK2kaMh;GqLQ)-lP+uOW2a9Q*&iW+N8WG7ffZQ2z6su1 zi67S7UWo=wbo^%Eq(T^=mD*b?+4Zi}7CO^oevGXKwd&Rr3xwWx@l*;^GBU-jeW?+f z-5m#LTpSOkI*HgXx8VoPtp?-YN^i@g$Pv+6rK|e_ObRxqjCU)j{^;0rREjwH|MS@N z!znOZ4EJ~}xZ5E$jQ;6n>e(||NI`x!Afi#qkRaC;&KmUjh~Hoc41VwiI#gx{`J{>Y z=c&^Rrf2uppmA7~k6>^k`*}3xj~?*lowCG}Sy~7~Gp2zF;*?9n7Oq*KLCUFhUUHwdP77=W{zyHRYT zWh17?ldG-@gVIy2qf|*NY*GcUDq67K+icELX`yBQ;A8>fpEOz*nhR-k` z45Mw3ZqH&)g}-mt;x18KHi(N$pY z;FbFX{g#F90$`IAhTJbtzgnzyE~1>>U4o`)klApVtc`EL{9;as*GWRwGYh6>4cDOI zST|s@@_Se9UbVu?MLy8-ZEjD0%2Um}@)N#;H_9sqi&84B64%D9JgQYeXW?BB=ydZ^ zQbDfPrI7l6yDBEKdaR-%f>m^nU==yIjQJR=Xyow;2CSl*?0>;3Y6E8fhE-&911bIm zs|b2NyZiXwtaL<)z3Iu}aVL@w9X=8zu8vFIkf34tK@2--Br}PA&O&b~k0Cb#UH%3lj*zbf| z7$hu;W2-mRY^>Doq;DV%tZnX3J=Ap>j(*0=rR>Zvnn^7zDvP+P0qxzJ&_-l!nU=%A z5pZ?00R50B|6gw@swS2)RTPU9-3b#v*#5r=G>_5LK zfv4e{6U!s!9s>-tuIq+pZauOSYAv$-CA#DmwUfS*gxTiN)mkh`J50Ylp#O|9r~~S7 zWfXiZyh|@EE=qUo{DqIbgUHgJ8yy68_MXW*=V&+4*l|a*;R-dFX7?-&*%heya`2>1 zGfz0D28o-LtUe~xY<)~9cA`*5_hlWK$Z&Q;Z2XpJ&3(EoeTA#TG^aiyOMrJrU4T9n zEQ}S_Fx5002Hi?w({;AYSzrl-*yb__ysn0?;j7g9)0((}SmULdM?>sc_5`9%0*`uHyi1vh~;Tqsc~a2u+0jN|e#u zRWC9s=*F~y+{hK!)ChLDm5ZP+)FlkIRi~L}JA*?Af&b$U%Y{-9xgMMTBUY5BePW-hDji3n`$w@lfeEh$BS$;QI;G}~L_il#} zmwJMA5%lNR0n93L!*dgvax41HW&eSLX1RA2Q7f7aUbfiJ!)ifvP{Il=Uft)?GX9P5 z_Ij5(?tBlkdk=Tli^+vKsoiBhfSb@ z_Twv&V$Ll8x${ME-Fc__vF6n22>(P;wH5hSLf1_+e>-#+57&tv1~K(`(tbI#yUYI;+WqpK+j404l|hTeS9|jENoe=g1bILXz@C3R zExRDybL4K(Z&A?dMOYf6vHbG*V=Hqt&u`br*a80t&A!3;1w-gEgns-C3+995Bf2Bp zfHBzM>Pw>lv=vijE2c_noGTNGrQJOE3R%Kf{PPu9k~`k$E8x&r{PPv!kefE2WSZmg z_ryQuNQpDXA#>Ck^~68s&>F_!Im4FAXbY{n&DgKk?uEV#D+}~^fBz**-U`vo7bS9V zwg6#`z#%@PL5W z@M9v2=6nbk4-?82@GXfnTvPV@9}`N4Sldkfyi@g*Cb7uGcHz028z=KGUQBZyKf%|@ zR#ZhDj!BJoJ*UY#mR-C{WN!&Aw=iYijl>>mBt()IP}{*9uL-%XsJ}Q!?klq7`qCTu zRxm)Vu7c+fxdaRWjK`dfVh){(h0nuun9BrvI%wtupUU1Ky3sRsWlVgiVfzW>1!kMy z)z^XkvXOfYW>OQMnizK?H8|D}CNSJn_vR)U;9SLDr+)w;?)fuNnWm#8J8)4BL&ko6 z*CCB5%a_sp;Y;i?^I+ES8s@^>VXuj79M%oZMROavfw@>|GTh}y=my>L9@gK@8;H80 zZZ6-95LICG4S`rS+7`}&FVl4QN=#9%(zL-BPd%qjfUTa;Xf)>meT}-U-HvE&-*I%n$QW^luryqf+ROvrWFV;!uoM^OQ1oPzg|Vn71+9%%#sQ#Q1;E0pXt&~D2)f-*UC4=i^h+2nU74w z9;giKL7CCPATbC)48Ml9FRX zeHVs>28yav-g8rIG*}dveu?%XcNSJM>4zz0^%_1eey8y(?^41l?lrmZWv)B8z4^tCEm3Pyf&`AHT$< z$1ss*1{3T&)&Q2$@OLbV!C%W*6vMQt>rXI|S*+^(HiRs4)HsG5$0Kv4?OAp1hailP z-m!cY{R}!$(`hz7MPPSm*E&&b483@1PbRAri4LugCZ zxDkd6Y#+3sN~a8GFXap(0qv4YKMrW;Fx&kB31}C{%1_J3j89#G zf!7o;hPB;IH2vL7Vgj&ai9*Q|InuM)r3)x9V^{)qy}yaAk-F8eM3{UQ_u?6}HO)Xt zwgzF+j|dYLkG9Q(ZL~@bPEcr-?;cNr6L`qM3CNoW_IRp6-}x^Jij?);7=D>S-?1Zq z2kUw~G^G#{>AOzL#xvn-S-ylWH$b7Yx+~KeHU`@V;X%RC;n+TauEGqOo$tAbzzebJ zJ53PW$WDS$47=@ybfAW{sy=n@X6}hl|3vM%eCmwP+|=>dIUYS8QAF2IuH-X5Kqq=1 zN1w^ii7XlOdJn2#b4+BART#@R$)Q9hufYW^S)DzvmncB3VOZyxn!vFA2O~sPa}-~y zhl<%qfN?$u7q$fp{8VD*nN)Em?Hjzn)y_(_aAV}M2w(pqM-2;V#*bYPDkBIv2Dwb% zAgbIJ1$uf|v76=BBrMSuk1g5kAlN3bBzD0P61yPN5W8TM9J_#(;qf-^f^W!^ZS)QL zVi$?d_yxO%Q9rI{!CcrKbu@;h3f?iS9I?*{E2BI&T(wg@)DAJFL$6JNy@;5|m4+;!kc^hubHrGYiF1|KK#ZvH<; zgUMTR>(xeV5sW_5aLE`Blq=F%aAC^nKEG3#Fegc+DkDGtczA#W+Bxu$m2p_pk`gnsf)agY999*d zn&8S9-W2&I1~9R)u#71L6LL{9u1Uc@P`r}H@r4BZbH~rr*Yu9Q*fY$dPww7-B<5s_ zLy@K+Iqxdg=pWw48Xjf&*JHgS#eGksgVNe{0wU4ER9m30uvW{`KO>qdqE#?pTMgWQ zz?Q-^rj$ip&oI~;z-9o=78$3LaKWs<%Tz2?Su9ls(rg*@sAOLqV`}i|er$qk4QH+^ zShNI=7QD-TTA1^T0W*f>6OJB?X>5MV7Dpj0f^(dFHfQ8AzJ2tWcj+Me}?2d8B3wg8hZTM9zxw_~W#$MWV zNzJCjU^6K^CSWmm3)n_Fe>(j<3H4*^>0@ty%I6{$6?)u~dCEI10dsl8o`FF4H4KM}hW zESeGqcGZ$;HB0xx%*!+Hv6Ae$QiSU@a}2}==KMKR%) zxQY9;>>vi4WAP)tmv{A+m{nv?8HAzys#3LZUDzTP)(MBBW7tUIrS`6Vdd>+tsaY%o zU<{*=&jV8#5{f4x4Az9PSKY1xcg6WC3$q0`+G7Lz9iP8H@8|_9R?NjrME2TFx@;=N z8t;(O73{HKtGh*KY$y%y_8N88 z-yCOQig15sK6YsEZ;VPBd>uwzXMZw&k*_FVcYIG`5V1(j&$#Hw%8x{7ItCB)h!hlTVFbXp<~6KU` z)V|{Z3pO?Tt1%2~BxvD{tOhtReFb~LogD(k*fS}?o(T&yiHoLa#07SV?lhZFVpYuk zAM27{IRF3vc-qyMYiv|S6vxlZ)s|OVC{hZorEmJ8TiT^iuoMCn=c6tBOTovoYAZbPX3 z;D=xSGk5NtxifRlnKSxjX_2o)bgp=MzO?FT64Z~OY0|2GMIWcWopLgqlW}?$dXeYf z!d7^l_8Cqa(F`fmnUbwH%M$I0cTE|-uxA(d!AU6V>xC^_l| zZMhPXZkZ)L{&(s(%D#lWdI{+Q$g%H-bTQww@%>)ORw2pcn|bPt4AZA1Qyr63Q&#)< zPcxqZz^j!(c8y{&YZl>o$usNMZ_L^!f<5V_Q*zlKfhLvg{{_E?Rgz(zE6;+5pgd|H zA2xd#!w{=iB&Bzc6JhrJ&Z+hmTCGh$*q-8HY%CGGq|Mr2EXE%D%g*>18;{1=dS6&S zh{o8<9OktX-;ja98zEDT&&8taC1Ct8g7wG#f-fgR=la);pF0NO@55lKbo+n8HMmY) zh3hiS#)EDtc7!6a!NrH!YB1#|!m+|3P^psGi%9H9fR`){Hm>voZ%*IsHdeJsk<-~> z{987DC4hJuGO;ZVgH5~}Fo>bi*jU;Z#Mx+co=h?^+U$|Dr6L`WYCTzFxpWM=}yh_p)dqy zI}NzD2h97j*l&s8O_74T_Dg}6A`M;%WJ|tRh)$9crv;Wg=FnyR#87RuTaHFOi{(D2 zg^qkZQ8KZc5^~6p9?m|@XN@LcV@>$bLGq#-lA+(07WIaV!iFn#JHFITPSGyojk4~w zFiag~9oCYgOv5I_)Hx#cbb~ie<{I=0a)Duy^h_mGpe5%j+1a2dR!mo+xMb4=Gf@QdoB)ZqsrT1LmcgJ?WUhCaWveX z?q5KUn7a&nEwVL?Zo_Vp$=OXlY4Z*LmN}Q*KW(sg9&@iS`7k+!z4wsA*!Ru(y@j}R zi1X>+*cI0G==!L7M8k2p!o+xk$zRkr#I02N?V-2BcenbO^HZZAx!n3e zSU`J{)fzpNwjZ#;Gvo(zh$HN-ejndE&fV%w&Y`o!55^e?^a<84j~vy_Ar@W6KSr2N zyg2LT*iY>GB!YG8WzB8he-_yL5B3mQZ@1=)&AP{s64BoMNQBv^J13xjy3Kd7fp}QE zNPxGW_!W{iV>huFJL)%!5`b++V{NoAjQvJo?1&twRo~D3C4>FFNzSyAxPP3Oevcr-*5SuTMCu9(tcN5b)Z4T0`+Ua{6In1;4u?u|!=E5%Ohl#<5D9_~V zdI@cWi_i&2Xn)b_IJFsF!MV4B{Hs%@s8;`Pd?7`(Vn;K`}8By&Dg|N z6Jh=V1ZQ{&c-kG#F=|3V6ouh)=Dt$U!b0K-G|dySO%|XKQb{)OI-Atmy8sIX6&pib zfSq6p3rTiiEUc{3r1M7(^KrPtTt=2u;>aZ>H8~wwUG=aa)_^1piM7-UKZy0MAN(cO zKd~uFuDVLExkj(Mjtw`k=||?dbNY*4>3#RHaKU8g5q;t*&OBo>_Z$~qF!}Tbx8CC3 zUwH6=+oO+iI;S&U(-r@srHFR4@hP71E#BoA#~9bK#$D|26eqmIMb64uh0iBtye~6- zV2kfteBa_5Kyxigc-q~V3v^c1mB;tvBqSdpMGOy<5CX;k5ki0vF!Bf?Jj4(n1PE_J zAiP2nl7JBb5fPE;IO|vOrckO& z^{F96Q!{EwZKyq^(T$Wry{SJ9q&ylyW2um)&zjVoB)?9c`Ro zoMJ37Ru+{P6zUw~BICWrhmEU>r%o==wZ@IcEynG}UB#t^#rn4Ku<=OAtQqBc%y`^* z()hXYOeqSj_H<+nHAWa4l$K8{aZQYI#sp)MF{RAUNi%jeW*Yk$b1I-imuDPrEH+jd z7gbCxDRTE2A2cpEt}?E!m{C~f)*Ck)w;OjG4^~v9w0B30$Bf5~Cyk$j9o!jXwPy$! zLyZw&$B+ibCdN2pf-wn94M{Pk8M_)Yjs3t*Avwl8<4EIpV^PJd2^As5#&TnoalUa; z#jLW5kfp{4jjN37ja%nTDlZM$ZQO5s$M~M{1MFMDpZ}pU96S7nONIxyG()hDLa@i{ zU~@OUsbJlYs#j5LNBn_0JQODez zU~L#rv#85l9xPAT2+Nw9Yh_bS=>`SMX*FeNSg>pnEW6ru8k!!O9XdAHR}?H)1k2^Y za&xfU7A%hh%Xj~#{a&y<8Z3_m%P(rmI*GwDy+-yrgKEmir8WJL%kV#?sh-&9jbOPg z5YLklXVZ}Z2!+Zy<9V>Y)$3b5-sZa4kt@!V@`|BloByz6x)o89+aMoJ< zS6WZJ)x5VzLlzT)vH?~qQUbThZzApIR!Hkkxiw;+H&yx3CN&k!`2?4<@6NnY%A@8ln$AWXd|l? z)z)VCXIX7-ZQoP1wYO_)Utg?w%kxh~Ez0_!ZHnL+e;;mwo+uk@T{}OecK-0%zM|T` z+Nbg^yH<(oEyjOSFs9(G>s8Vr9ibz2l#bRhI#$Q&crDNgTBs9sk{0P?ouX59noieZ zMINK2TBb9#Tr0FvXX$LcMXU5youjwuT%D)$^>$sLcj!XBQy1x7x>)blC3=tkSpQw` z)&J1@^nQIn|5KOgPxL{3NFUap>d*AQbh$pFEA&xasgLO@eO#Z=C-rGvt~mXH|h(zNng~>`jT$ZmvyVYqT6)4zNWA1PJKgn>6^M+_vl`IOW)Rgx?d0I zK|Q30_2>GI{zBi?U+NM4mAWxOy(q z{m9jKSGfl6Y8T}ix<;FjrlXSTeGY}CEK7a( z#b_B_!_V*BdYuZDD8AzfI|U;h+k zb6yU%!*#8=^q;oR{(t$!@6*iXwerLM=3mvnwB|qZ7yKta!(Z}Q{)*4>*IfO5eJ)p$ zy@ks?vQ)m5ujFg_M!uErlvOoE>u9*v)q2`c8*7X<)n?jUuhr|crMA}VwXL?(_CZfp z!yfQx-luu5=KY%YY(9goqO-`cg~K=!S#~tXb0Q}r&ratI?!(!f%lSN(C-O8d$-vC9CA_}?|P7Gfy>}W(3;?Sf`u*9jzGAH-Xb#f3Vp3Oml|I220!ku+-&%Wo{ri(|ONX?gry&BV;;1fL zFQ3ft@=1x8PfERfQifUMWG3(M5=uE*`C1?qUP7t#63Q&}O_bTZ*UKokcp0V2%P6;c z8D)-_QEu}xiqAHu%RDcm%=a?N?OsM%;ANCMyo|EY%P4nx8D$Z;kyn5(@T1@+UJ1U) zkAa(c75EZA4sPKmz?b<+a4Y-#`4xT|+y=iZTET0;N`4lc#cRRYybipD*Mn939C#~l z0O#=Y;BCASoXanO^LP_DpI-!T=gr^(ehIvT5v_G0zYN~VTfs&A3V0W90~hnF;N83( zT*9w`Kasn@2W2t%G`|k6=AGa({06wjvhQd4O>iym2G{W(@O6F*+{ykNu#5MBZ}5Kb zO+Enb=7ZoKJ_KS{khoTez;#>_GKg$M-|qZAe}eY)$R`FM7Wd!}_$1nGuqSe`OEUP^ z{5!PUBG(v*INg&!aHQ9F}=!@ozn9rBRDh~d5XIG;j01zAZhB6=2o#Gj$v z9y!Vo#P{CVlb@sA0Xr@ad$14xmj8fuN8~R<5&Qe{$9$R`a+!SO0sT>%jGU#Lz*>=| z4gov4JTMIzfkU=444Fbd)F#PrxzXnURBjQ}pH`A;&OR=m+G-jneQ!pDY{;^rEAwcb zwFjQWlwi-yr%`l2E#puQ=S=QxXR>^{AMH#(ODc@?p* zNInM$yLht1c{U!W=mctMU#wI7UZXnr#~>q1qmL+ByJ$M?h4;yzeVVD6v|qC{iw?jW z^`V3Q>x`U{v$%h?oX0cpD0r_>4TA@Z&?xw@Xn3(WjfW>o&_sB%By9tamg3LCr2Gya zLqb(w>Qh~5AXnq`EL0D2b;9#cK-1COhTBpBx8v?q2wy#oD!7=J(F%TuAEG^aqjrOI zJ+&vK>!rOQU2p9T>H2D4ItYJ}PlxO|zwpoDZ}>a~l2-8zDj4>>>F!=3`H5C-B z=m<#lh&)P>!5Ne7D;HyAXw6(r)6V|RHL|kf5XkCfiuLn?8uZrkE#A(*8SWdD;uBb6 zz|TVaN~8YG=9vnd?C4id$BZQ>lyh(=SWF>>tb)Woi6 zfgf>0oZ%VS_DstW{ntGIt+t%zniVoNw_GueW}XHh&D zdTYeF&|NeSf&OAS4?2wFq0nPI4}&gS@Nnoef%Bo$L>>XXw&Ib{Z4!@ye%tV9=s20j zK+h>W7CWLlkHe1W%SGP87%|K2wo_1Nsix~5W)nkAx5I5s#+rQ;nH5YnTbyZ@Ut#u9 zX?8gq+HUK2?f0I1VV@rVjhR&ki_WRf7aP2ISJ<^Cs2_y-(Gv2nb~XB%&dRSn%O5ICZc%y4sRrN z!Ggt0DQVTU-&#W>$w59DBljrONTyWMbFZ2|lE%zivUDU(S-RxnkyPr|VTz^!l;VC< z6hjF#(EX+dGNfkn?g&q^yDAf1On{T^;IxJ-p>IjV$nt=XTZNt+j;S}_C&fpun-$T)BaK8%I zGF(e=U4knHqc;@eKAs9_I>yy6;{yNPS?u0_?Z3xlx%cH1jS1w~c(hBT;u4bT>3Rx$ zxTJcHm6AZmXd0NkxZXQU_q}Jao%iTF^cg!G&ncYBnVikzIiHJoCYSO8Ud&7QN?h0Q z^?VDj<@LM)*Im4k@8?bYbAFt+^3(h~-pRZ8Rph+Ed-**s$5q9(T+hwiu1H0z1T{zv zRat5b+Q?TqssL9puT`^E86<607pe=nTwQ`E%hgiQP3kJOT-~TvsdY%p)pu~+$vf5e z)qUzgwOKu?o={Jz->PTTAJj`~w|YzMR|i#vs#eETqw=W^^`KTdRwrqX&d?+A%u}e> zsH%~x$WD8QrD`iNCkY)Pbdk^^p~Hn{3B6Y6d`szW;jb1tO>%x9{7Rt@gp zg(kAkY9)MzNb?GxCH!yXsRVAeBk$K!y(9D$lpa%sw(mm6sX3s-gpU&%FSJPL1WPd^ zg)-Wfj+C5xg`X!h&y`asvuWvAp&3&1GvUVwUnTr9;g1U4E41CxSx?ORwfKWn@fTTg z#Zd3=GMK_RriCRjn!24`FGUk{a2ch{w zuMj#}XpW`yL*cI$I$v_`7W%2=Tqtyu(A7d03SBI9?iIdR=s=+fLd%3Ip+AxvxPq{{0ias zm?Y3@$+=kQ*M$xfI>?Mh=`+Ib6luqVA1gFn=m4P;MA|;#bA>uW=LsDrbhuD0bd1zL zEc`OzKNk9qr8BOZ;kUg(V#6M+=$;_^wqCOa&%LYefe%PjKL@``a{h}OLAUV<(09~8 zNXnm49E6o3xja~%i)l8M(S=yGmtqxPPB((CqIFn^^#sHg_UuztHasI#vn4 zSZJBh$AtbsO1B7KD)ec|X%xCia_$v+yU+`TjuSej3xmbJ={vesd*F(Itg~~kIg9Po zA;PkAn$dj?1T7c(0`asR5+4=QdU^`JrWM}1fG>g%xQ@TY_wW{ejQ4O2u%88d-=e;& z9>UuCyee1idZ;eem+3q87X4fOtlp*H(p9?A$#zyb_c?o=_nbpcjZ^3NqB5gqMlFt7 z615^~ebnPo&qwXWxTm4jbW=-{O&t}Q8qTIcJetN}j>hnLG+d<`uNqiq}t*jP2u@6s7g{)#cm4Y~X zvT4LqK9n9*qjArnSdjiNNuf#>Q_3V%jc1T3a)_`KQ$E;<4W+MR)^H6HeQr=ptKETX~iy9|TqYgFdpxqOwRRis+P_vPb z;_laQA4Q>Nt!U?Op%%Fv((}5`_HECj*b9j#kkgDDAL_T78q&yZLyrAcM{(SQaxbL& z>(z4$`sBmC)%=y)F*`ps$DzF!I&?|$T1hBvMX6UxYdfF!KU;;;`p(j-9?w>w)Q(Fd z#_&}RK8f1Uo!d;at6@#2oDS6Vp_Z*tD{`V-8NQ&B zDquFv9D<;DD=A&-yeT-l75z#Gnr~|- zZSHIwM|9leu~+-j=PW3TNo^r}Rgwq4j|(8$}q)zd((fOeIX#fNYmhP*1=k63%blSg4G zP2jz#eS$I&NhWa%c@T9bp`8x4Sg~y|@S8|VuQe@Bk--WD`jOBUQckXEcP1k@&dC9n zYucQN#^>bWo^Osj1*QR|EzT5k(kVpGV^E_3r5z|e;!MI51*i$^qQ(hwPNwfhPiGCrG<(w?lF}#iM<%N zB50FH=gbGz{YAFkX#R%VC_2YJ&}+Z(nF`F+Ii%A$qsdtv74wqN-C;^=k={Q`nrt@* zk+y-I6{7SN#1&)?m=C}m{{m;{59f@XoI9HQ^$Tg$hiKr`+=Btj#dX;1K%F>K_c>{B z&@_R1h3_z4^SWdwV+9)O)@X&6R#X2suW2_=;Hg@(!@SjtU=LCfSJ+Q9G)S>4e9-e=iVpNB!g2D7b{$=;R}Ff&OQ|H$-6?(AU=y5gGa*GZ0HRNY7t^w%%;BIr6)N2fY@%Gyy z_&P+)8MYU8ch_md$E2q8(2lKWzh4j*{*!EX!=~Ae2j;%bO2%^@v{BWC!#;+|F`u|x zdL(ubrlie3zjE)Y71N8Xjfi9Sm}kYdDonZgove6X^M(}LGcb&U%zLwcHZe{s;7fjm z^jM#oK5LHpV^Xut{LYliJG(cq*Nzm$JIp`LtL|K%uEjP$W9xTb?UtR=hsCxB`zZLj zXgrZJ0=zfaNru?lM^aqw(DSZzbVOWH9GwvbM2o#`i<;v=xSh^@@u8r!Q=a?2walg_b(gm|REV!QQj3?O*3eL3aH z!(rzwTH+#mB?hhYZGEj2ec-9C2P&pYd^idGJN>kSw$L&|`E8822i_zDD`AD$uT8Q; zHZOL2WSrQ^k#Os%{KCZV-&2Au9ODvLi+#QCyv<(%x|w$)*b#(BV0%CPPaqn6RU$<% zB9lkamuPsyCi5gp8zmz76H5P%5?jB*9l24IkNcm($UTZ3Y$0c# zyV+;{28}-qtJ^OF^JxC3&oQa!Wi1Y2c>zBkZiJzitN`eLWA~T_C5W(~la1z%i?{Kb zkK8fz^cGROX!FleOy@2oM!Z&p@rm_A5^bG`tZq28*_ffHWyoHt_nlX5f6kHIc&;C( zQvP_a@7UhIt8u+iSd4T(Kf!EzB3VUFS*2oo-&e~>IuZA@pWZA&shcsQ73No9Gt4pY zTdY?%@0@yef;hn4>zMUDJVzVsx(Wy_NAh*`4f#9G+wS+r&C~XLst-Hy!Dm45AJy}|wrbLDTbAR-$m54HMsN)$=%PO}9w zpDN?6K7ESaA<9?skzjN_` zx9)pPI&CY0zMI#&qCh!EstL8`_w2bJqYXcHBlB+_!9(O9hX(l1 zbAqk5lMg;YhzlA2KI5K5l6e|FwHW8xz@qYk-` zJVF10PsmjzzRy*`A4ST}%kDYdNMxt^C*nlA_o|TZ>-*kS2Wb+*NBS)dGVSSkU0=S# zY?IwaBHDcZjK-ubw7vC9#;1|K9qLO;-=}eXoZ{JcDw&bJl?nSv5jpQ2d#-fmAN?9xEu)&)%})AdF5w-BMu$g{DhT)~1>~`|m)>qWye?R5sY?S!kuCouCw^3r>$Gt1@=bAItYrp(* zt#6n2fj;$k!`k0Ssh?vK z@ZxSn(g>V+Ap`%k#lr!XM1fCc-`nTw8^%R z8plWU@MV4byw4R+AIiw>`h41#bC#OOc0b)$e^|ku-gXG5`>RaI{gc_-_X-G${M+X= z;tf65n(!kmD?#wdNYC=BuAP1V_VJxw%6po-`!Dc!mc3~8lkj8J-#uvLNCj<39ok>*R!&lUvGDC_S z)vS}TykzLfl&K4KArhJ#H)%E< zK`Nm(q&e(i50!Eb=h9rhlrN`wd<9=Y3wQ(HK?`{kZ=pr}3;qRN!cXybx|E;e=jd{N zkzb-EyodMDQdO<0=}W3!)zdQlWxbBB&>Qtex>Y}^pQLsAkNS`FO!CUwJsXT`pzOz{Vl{jm zsSrJR6Fs$knaa7)znCwF{?qslXg(d9Z=xBz8T!u@{m zZ#$%ZRbOkL(Dv_sv})BPf6HD>G3ZGerPE~e;Z}NBrSqTFFg1){Rm0VA z{tGl7!MmaLD1J>QX^nlt!k2Pcal}9lj@9B%bfAfcy$xT zBv-A#XiQc&+woAhU^HIBr6>WrSuMY+b12!qp~NiO*lRZCCx*_aSfqil%{VHR)7!Z; z0BIhcvwv}G&lo(EgeOZe|JDKqA}vJ8An=2chRSaMGGNz3U>&JQS(x=S;B%<__ltC- zf0MuM%%)*TV^Av-vojp20Clry9BPe(O^rgzMXhX_h&dchlVle2FpCqw=b)?rB@|gOtM!JHg$X~q|B3+5{BAS8nsWcPi7o$&$k!C}q%h9JL^d;!K z3_fHw-9)R=vo)}oQdrCejKdvB^XN{bLG&L;9{MiQeEJ^U1OA6}KWaVzuaipujWior zeG>h){wI~T(bMStGqAi2dXD}VJ^llHQWlWBmqyS&I)EO(OXcY62XqKyau_LKlIs&-&6H=Y8Fi-d9OResrNHTB zd>QIr&dX@Bc)S9>lD|rY_V4bg2#CA^UhYO-3E#4cH$cuEd>4Gn-Fy#x%MbZ}_?8Fw zA@~;SX9|Fg&8YJTKLUOWd`>a2@-xZ>S{{XjU+_~@2LJOK@_`&^0_0GfMGnRCC)`03 z_*4Fj#1MBcL_5&ZQm8KZws3>^aXcbLURE&zDWhz$1f{#;i;1g5=Em4Uo zk^WsJsU+~pDj9r=8bX)BYrwuOGU-x_Oi&;b6v(6jz+?sFRH{n&@acLwCF&V^27Kd8 zJ(E)PEIkX}4_E~))ukw%tLK6*(`7VB&(rfLPhX%fpxJu9o=-V?fnI>87V3ql160#e zp!&-+OJAk0q9l0uuTYA-y_7SYNNNry)T8O{lX1 z{`o?Ev%VSpE&3Kp#|p8MhUryUCx*knuBMCN0oTxC{SEyM8l~6jwRDNTRo_aZ^*VU< z^Yk~d9?gOW`4L+Gv3?M(|5R^+G|+c}TUbH+MjX@T`VpzuBvWxWr@!}}DH;}pRs zYfSIY#Rm-(@8c0);}QQdRJ@5t{D?==An_ndJct$#GFd!`BOatsJV=yykip_X>^eFU zwXD}j6|WHu9F0L;Xa~<`y+#SJlY=_BNG0Ms(!_U^i0_yqz9U0?$1w37)5Ukhitm^y zz9UY2N2&OZf#Nkh@EKnM6s^DTh>sWwFR&H7Mf)H@y8_zxq8AqDj^I2>a6VXY&cJp% z^#24%P7@?Y^GMF2bU|_nPvnUdgS9Ci{4_2BEd_4V1h*xE+c|>R3?TL{8YWnc6|BYy zR!jL|ei#xgR?`HfX+Y_tlq&c1 zohOJ*5ya*SVkZh>rwU^81hK_}*lB{;ctPwmL2SGrcD5jPt{`@{Aa<@Gc9tMEOJ%4G zxXZ zP+BG^og^qt5R?`ON~Z`)69uJ3g3=^GX|kYnhW-&yS|%vX2TK1F{KI-HsKsi&{;l2t z-XeAY5c?YV*Y#e|eL!ixptMX-I!REP0F(})0zqk_pfpKPn(T~o#!;Eza~vY3e4w|$ zDWDv|=cxY&_s}Qe00031000O8000C44gdmaWMyx1Z*6V>0z^hkQ~(ZaVRUW)4gdxK z000000RRF32mlNK0smG20RR910C?KnnF%~p?c2v^v+o&XA7tO|844jhs6?R=m6S0i z#x~51ExV$!lT^qOvOJ{~F`C@B5s4 zIrs1SUe|RF3*T#R~gL5DXVlk>vl_CNT$MOI! zb~bf>Btn7>1_g;e7JXQ|@M-||KJk^MR3Ibm+T^ z@d0uyouSa>PH6dCkMp((9g2vQU#(!_!B6Eah?Nuou?T2J@DhZe|AlALf)7-6&*|R? zL$+OL1bmCLgd?E&>|jn0xe3KCjM#+7iUAQufm@j0kwhiq$sT?h7%M-b7FG`|XEg8# zYnl5HXf%ut#xBs!hfKuSQOUjpYAD8>M5U8=l8FR5nc{~r3#5BesAPI5kP+iESOKs& z1Hk;*D#nM?2l`Nj&T@n023!CA3B=^Ht{Kh-L&hY-K+JwbObo1b+m^gHk_m8hjo<*v z^x;#dYA;LE)IGdBojMO52z9(fY?f90pjKwVR`CKU8xoawpf(#;p%Bqs8F!3md5x;v zHf7Ti7;wknmF+K+fdkxWEh!kvVS$g83OoE&R}{C#51-1$+{Bl}4I4K^WX~+K)aeTn znGiS@;BG_Git)58MIa$LeYYh@XkK+TOVm3wlwh`K;r`>FziBP&NgR^@^{tO1t@cUJ-SVI3Uy3+S&23$=(mQ`c*{nOCJBUTT zguYG%NCX^#5R5!9{yDs5!&jVv_#!ws_HPw8KODC(8XxLQ@S~H6 z7;_5MpF*7*4gjsW;glDf83SVQ3nSu5)F3jEM1zLLXkymjAiIcGWyni4AXfQ%KdpVaGzQ3Y%|jSNX31h`~&2@B-Pz< zO|Iyx*INzIy_aGpW%=ljGUR`~@!>N)T>1Hjb&^q~SMoHg62^`{uTU0^AbdC+AawDG zU7_`m%k&U`sYZSU33In6Yvg)($%eHSv*J%II5K$dd+v#L@d|ObY2O&%ygBrB z>*WBZ=!;^eLCgo{R!Z}uL{aD++3)Pe^juhiD25NZBebfy%vh!kI@#-xSe@L3cmV22I}I1HFD>SR&+UoqozCU1uB1?xMHj z_{MzEZX|GckP;<*KRq^6tl7Z2Tmr^!B%_KMA^3=*Y}cNQe-c_ zJMtWeMWsO&|7qUhCAkbkT(f%h>Wb|%S6|Nx`QGCH6K(^nz+xd!v1r6tsHnGK#4$Ji z<_zX>!6PiX(13BE2GZ!54J3Lnh3bu63H0Yqz^V{cF*pELV7~1Fvg*|oDuysOw_PN6 zOdySfq4@cPVl9FB+!h-xcK*-S$soq^za&QWGEPp_UCZ-ac`Jsk4L9_KUo^6ms#AYF zql)C43B4m?25RH|*fmU<6m^(>jVW}gvx_sy9#_3Dwf%?GRa zk9KvMU-Fo31gVj6p$ofS7}V{)GXky9HZEz(u7P zUDBXFgM)*=?UU$FW5HO|T~cXN|BJdw!1dPLU>Z@agVh1Pix}fY5x|Z@mjY+71vr6C zqFj(VJV@Fe=W&b?B}{d}VTq_mz%ludcOAHNwQTfTF0v{K|YN9+YpV z5kzCwF?!Nvp(b=oz%mwUE@h=AYX?7JQc-(^%!g}%$!mulb!}j2f#vokj+cEe&b-6R zj~+f&nH}jbb0)>m@@f1*Y_G4)XI;DKtoCJZOb1XVPe`|P>#(GR-Sze&$xv# ztM(Nm?yJePTMzaV?}_clIiK8qWNX<`6+3>hmst-9hL-ly9ZMW}GS+WPMf5Rq;$=^U<+B# z?lOAtLw;vmqGO^?{R854tD`HMo5yr`?z}FNoJwqXaKhH&UfVG%KK%0@3K7M`Yho zf}yz(!Ct{nOMpQ8L3-c0ZI9~pWX(gKhn#I! zL1Bt2URz(?r<%?m-6`~?e_N2B?RI|4)n~!gkyYRTKarts;L@-r>C}-!>dJdHMFfHAIjHVnF-0XzY>2ccAv|n zagBXpsE)c;SZyDMOE_TMOWe6*qy#u=?6)$h?TBh^TqW;G#Anx&>6OjWMo}4$uUwSs zK?QA33>4--6w?A4a~e_nA#^dPxRaRFVPFVi99Yb%fB_5rAjbw37Xz}p-!4lS z2+iey+~x!y@=glXk4(S{F`YM*0dnFAewcL>8VRca1WDhc(;DfQUSX_c8W<8lmrO+rl=@>lp zCzwDbVH}}}xX%S(LAZs1k$*PuU-I^RlMvoTCYTlW5e;Uxj@csr{CTsP4--mZGG>7c-4fo-FD$2qOHrO$R{To8HYmgGh2xBIos zF>l>=YNK%b=%u?EIZ3tN+_lQKzuD((7bQ#Ied%#e<-;@H)ba41t=B`m-vy1~%m=6@ z*;H~e%;65#=e*w0rpM*xT^aOs=dZ$3p91R>r%X3Hxpt@Sew#Rcss5h1ElJ3PDk8T1 zMDJ_>+iBIjT)vQf!xPrI8O7~|62GT%=ABsJZ5VV%f|=VdEzE@N_fR#RS!`Vrujp(W{x+XYhp6aVlG581`OHrxW=!A$A7ehbBH1TQKa^hL=Fi8 zScp#o#J?FZ8M3hc$JH>9|3|BVc+U#jjw9t`VMWSN0X>DK0;k|!T@)r0O%X{|y(8S^rGGcXawuif{u>OGqex2frCTe*bY zyNl1IaCnH&y4YR&uJ(#2TB|HScX<%RvU5XBelySHjj*8#MaM4^@e^6CUDi6+eqi$d zr)-izY5;JosWYSaHFf^)bU=TF;&%$x+e_{z)Zs?EoIbpIc1GQR8fJLTmU6?ybalZZ#f zia1MtJ^8ua%PrRCo?NfwgV#^@7$&IM#yO|KZCgu&1f!IZ;SE)h5tsWvv0ap0n@b+P zRBU8q23s|dHPrkpGoX7D{2EMYovG2;LZx!vQs3y5G{vxvMiZ%V?X2mcy}>PK9Z&Bv zbO3nC0XN-Qna-)L2UOyQY{WK1I{#jL;7HU!`>W!>SFc3guNnR*PVLdsl{j8gY7<#s zXUl8s^cr*@Uy;%s9dut@tE4?rS9AXYif5oZGX2x1VE%IiB?Bm`{g$AP|A3&2;V=RL zWF(~iY^Q%nv;X6XFw^O10(GETpj@CBukdFN1~fgEGab$UE`64sb1b3&z35mFEH35` zBlHFFueYTOzwj?VHEH`yfEPb4vVDer*lJl^jFMD3dd*22Vp3h9ZdZ%t z2w$8M4SBmf$=$avAmh0uo2$qcdxI~V(cUv%#S@|4oV;e#VLf?vt_Fos6N_w_QNq!- zQ)%127~e^*{Kc5GqN~Q(R3$Y>utPzq%^GtvP1YsJM)!tM&yZd>r36#2JFfJgif_0% z`$J)kT)GY=eq?iD?i^_q#!65`34q{sWO9A+k%;M{-z=D{= zknp(X!vhsKmIDdxJq!i|;)`tvy zxXf4!1}J>(9EF7xd+i34JX6y)#x_pyJ@ytB67>2U_8qiQv2d85<=gQ3VGv@svYV4n zID?D*RYv_uAN6B)ZIKl#T*qx5xd!Yk(1`DDaNcWYFL#c}9jNQL1B^YDwiy`Ih&M-j zzD-p*eW*-q1@Y~4%$bxpfzji+AEn1-1o?#qGRT^-cUnoelT)iaicONZl*X#4)b%Y- z;~fvbJ)*Vr)1jig09oA~$~$e<*uR`o9cUIqZ&DubSUKT-$fnT*pR{@Ke#+p8T)^Og z^CPQn(?|FDHR`s|=u!<7a<9nb`)vmg$&+-t**MZRJr1uLXm8Tw8L1De&RnJUKFCw- zx1^)m_3jN*TdKKb?;DbQI(XtzuV1k%OH)_j;?&dMCQ=z1bZ5VHVJw0N#3DGp-aU3~ zEc_z`?kR>ussHw)te-qfW08^zSjwsyp0BU9;gDn&TiCDy%p6b`2XH!A=y&T6a`<)C zxaEY$GW1TakL{{)!liLbY&Cu&2jbR`Vy2ga9*MbXc=orPrgtmut-t&$o)~&}t0uzw z)%xCbzYp;xMEbU*=L-tTy)E;~&)B&wVQJwq<;aIG?zf8?>aBOz=^M5s595^FAtS<5LUpS3%USbM4IYw%q#d<)~Yp!co12r0kiLhyqZxTToltx$&%=(yqvtQ`q%#&cr(kC4{q0_#BYz zv2>r=r}R&m7mD5_tvq{+pl~CkOrq$p#&P0J)SNK>z>% literal 0 HcmV?d00001 diff --git a/panel/assets/fonts/sourcesanspro-400.woff2 b/panel/assets/fonts/sourcesanspro-400.woff2 new file mode 100755 index 0000000000000000000000000000000000000000..0dd3464c74be96d2285b3910222aec5750cc57fd GIT binary patch literal 86844 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⋘wLzui*CzrQxHdC>qAqVXWf(Lwuz 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 literal 0 HcmV?d00001 diff --git a/panel/assets/fonts/sourcesanspro-600.woff b/panel/assets/fonts/sourcesanspro-600.woff new file mode 100644 index 0000000000000000000000000000000000000000..8a165ecc26b7d0dea59af8d09594d62d5877c4aa GIT binary patch literal 74996 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 literal 0 HcmV?d00001 diff --git a/panel/assets/fonts/sourcesanspro-600.woff2 b/panel/assets/fonts/sourcesanspro-600.woff2 new file mode 100755 index 0000000000000000000000000000000000000000..2526d2e1b602bc40c347df3f78ce757a9b5adc0a GIT binary patch literal 86196 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�sdvaBR-19P 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 HkpTbzO@KSN literal 0 HcmV?d00001 diff --git a/panel/assets/images/avatar.png b/panel/assets/images/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..e200295f990d53ce16638f966b830d7f52cb9d79 GIT binary patch literal 324 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sr2#%6u0Yz!$;ryfN)4!_m=EP*9MEhsVIcKu1T%#>PfOL_}Cv zSV~GtNJvOePmht2(bCe=!oq@ug+*6aS6Nw^o15F(+FC|N#>mLX+1Z(um6ey5cdJd% z3!ueao-U3d6}R4AILX&+Aix~J%$dg#u;BN;@Bi&LO0P1R?x*Kr`{b_fO*fIh({J5Q z>n(_M+El=i`eWKOjwzoHEA=Gj8@V`yNo8%>{dK!sImd;(8T03EKfCwwSBB8aN5OSB z^M58ShzywFw0Y4Lr-?zI0(vw<15~CMX>fTNYfRe|%pRKS7JpSHP~`6ihOh5Ami4}g R@ddhy!PC{xWt~$(69AnvVF>^L literal 0 HcmV?d00001 diff --git a/panel/assets/images/hint.arrows.png b/panel/assets/images/hint.arrows.png new file mode 100644 index 0000000000000000000000000000000000000000..8314e23b5870a03f8cc88bb7480966b25122c7a6 GIT binary patch literal 549 zcmeAS@N?(olHy`uVBq!ia0vp^UO;Tm!3-n|9yvM)uQF@yr*A>1#8@B4ez(S-Sb;?qUYLGyG|eE zer)HoQfuKPV-=~J0b4hf3D1=kl0I$dvXX1Yj<`SS3{jEo9Orf3ueUqya4^wkQ}^%a zZ&UpPE|xvqyyI}I^s&gL#wyasUY1m(FKTZ*pj7e3;DU!^Xano{$WQ5s57l(8v?SX^ z>KiEdh^(2t*WK!dMd;E)oMuAm51Ts=>F8W;SkTrY_+2dYy^HI^vKc3I>_tugy8qp{ zqU?27-GhgBdb>*GwT)VD#{IGjn_ylhF`;T^=gYL-jo0mN_Wb2|`uUaE`=V&E89A%= zT(J$Gyh`Kyt;ZE<8=cMWY366bHZPPG|xo|9}(kkYW8=s-fvXm^ZfXVmcZi$=tiEK6H`U^@344$rjF6*2U FngD@4+jIZ` literal 0 HcmV?d00001 diff --git a/panel/assets/images/loader.black.gif b/panel/assets/images/loader.black.gif new file mode 100644 index 0000000000000000000000000000000000000000..ea331c36e805898f05af8e3f86a5da99b25237e3 GIT binary patch literal 718 zcmZ?wbhEHb6krfw_{_lY{Q2{spr9#Jrl_l{J2^SszI}W1=FO(2rd3r{XV0Epx^(G} zA3qWj5;{6Ma&mJ1|NpP}pWDwhB-q(8z|~04fSHkjfkE+~lygyPVo7R>LV0FMhC*Ui zVnt4VVv1g7URpkb;!hS%E}$wMAO>0~z`(%bBXGiV_1@CfgY7wok}~I(@VHDAR882> zV!`>sOm~e#ZkwBAGec{$qF;}sN`t%OjwX?)HytMe3pARS7%vrxXxCL&*v%#Ns8I57 zp1%^*0v-k(CZGj8U<*PgoGhBMXU0UqptSA+o|HyWX-fqKhP)OdnFB%s4;zddKkzjj zIG~^?X1Rpl!!$y})9LY{j0YSg=N|ZPP}snr%zEZ!lEMs@%S#tn71(nsI#veCT2@XxOwbW%exZHaU;Zu4?WSHL6k%YKwW;4>cAt=BczPxqakj?Gs`7V1NRGlSd OG<$=Bl|`NsgEasP$@f|S literal 0 HcmV?d00001 diff --git a/panel/assets/images/loader.white.gif b/panel/assets/images/loader.white.gif new file mode 100644 index 0000000000000000000000000000000000000000..609cac77bed8353dbad27ed75f0561bb3fc4c0bf GIT binary patch literal 718 zcmZ?wbhEHb6krfw_{_jyVPSFh?Ah+_?jJvX+`W6(#lJ4GIdXs;W{~ zSKq&X|Mcn8H*emoq@<+ypWDwhB-q(8z|~04fSHkjfkE+~lygyPVo7R>LV0FMhC*Ui zVnt4VVv1g7URpkb;!hS%E}$wMAO>0~z`(%bBXGiV_1@CfgY7wok}~I(@VHDAR882> zV!`>sOm~e#ZkwBAGec{$qF;}sN`t%OjwX?)HytMe3pARS7%vrxXxCL&*v%#Ns8I57 zp1%^*0v-k(CZGj8U<*PgoGhBMXU0UqptSA+o|HyWX-fqKhP)OdnFB%s4;zddKkzjj zIG~^?X1Rpl!!$y})9LY{j0YSg=N|ZPP}snr%zEZ!lEMs@%S#tn71(nsI#veCT2@XxOwbW%exZHaU;Zu4?WSHL6k%YKwW;4>cAt=BczPxqakj?Gs`7V1NRGlSd OG<$=Bl|`NsgEau^?e-7= literal 0 HcmV?d00001 diff --git a/panel/assets/images/pattern.png b/panel/assets/images/pattern.png new file mode 100644 index 0000000000000000000000000000000000000000..4bf0644e73e2837d706d3fea96a5e9b66671d9a8 GIT binary patch literal 116 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJXMj(L>zOlW{{R1f_Uu{C@?+b9 zf@+>Fjv*Ddj-D|D>g70m;KqOZsFtQ`**z;$G!)!!2H!}WuE1?46}o_7;fo)CjDR{A NJYD@<);T3K0RUq>CN2N~ literal 0 HcmV?d00001 diff --git a/panel/assets/images/placeholder.png b/panel/assets/images/placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..c82dbb56878e0e1235a23ff0c0699003aedbf784 GIT binary patch literal 98 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx3?wy9o9qTs%mF?ju0ENCzQ=D^f#gbp{DK)A qp4~_Ta(F#m978H@F)}gy{SV|aF)#*e&oKb97(8A5T-G@yGywp&n-$yu literal 0 HcmV?d00001 diff --git a/panel/assets/js/dist/app.min.js b/panel/assets/js/dist/app.min.js new file mode 100644 index 0000000..7577c14 --- /dev/null +++ b/panel/assets/js/dist/app.min.js @@ -0,0 +1 @@ +var app={setup:function(){NProgress.configure({showSpinner:!1}),app.delay=Delay(),app.content=Content(app),app.content.setup(),app.modal=Modal(app),app.modal.setup(),new Context,new Search,$.ajaxPrefilter(function(t,a,e){a.type&&"post"==a.type.toLowerCase()&&(t.data=$.param($.extend(a.data,{csrf:$("body").attr("data-csrf")})))}),$(document).on("click","a",function(t){var a=$(this),e=a.attr("href")||"";return a.is("[data-dropdown]")||e.match(/^#/)?!0:a.is("[data-modal]")?(app.modal.open(a.attr("href")),!1):a.is("[target]")?!0:(app.content.open(e),!1)}),$(document).on("keydown",function(t){switch(t.keyCode){case 83:case 13:return t.metaKey||t.ctrlKey?(app.hasModal()||app.content.form().trigger("submit"),!1):!0;case 27:return app.modal.close(),!1}}),$(document).dropdown()},hasModal:function(){return $(".modal-content").length>0},load:function(t,a,e){if(app.isLoading(!0),"modal"==a)var o={modal:!0};else var o=!1;$.ajax({url:t,method:"GET",headers:o}).success(function(a,o,n){if(app.isLoading(!1),"object"!==$.type(a)||!a.user||!a.direction)return window.location.href=t;document.title=a.title;var r=$("body");r.hasClass(a.direction)||("ltr"==a.direction?r.removeClass("rtl").addClass("ltr"):r.removeClass("ltr").addClass("rtl"));try{e(a)}catch(d){window.location.href=t}}).error(function(){window.location.href=t})},csrf:function(){return $("body").attr("data-csrf")},isLoading:function(t){t?app.delay.start("loader",function(){NProgress.start()},250):(app.delay.stop("loader"),NProgress.done())}};$(function(){app.setup()}); \ No newline at end of file diff --git a/panel/assets/js/dist/form.min.js b/panel/assets/js/dist/form.min.js new file mode 100644 index 0000000..778ae13 --- /dev/null +++ b/panel/assets/js/dist/form.min.js @@ -0,0 +1 @@ +!function(t){t.fn.date=function(){return this.each(function(){if(t(this).data("pikaday"))return t(this);var e=t(this).attr("type","text"),a=e.next(),n=e.data("format"),r=e.val(),i=r?moment(r).format(n):null;if(e.attr("placeholder",n),e.val(i),e.is("[readonly]"))return!1;e.on("change",function(){var t=e.val();t?a.val(moment(t,n).format("YYYY-MM-DD")):a.val("")});var o=new Pikaday({field:this,firstDay:1,format:n,i18n:e.data("i18n"),onSelect:function(t){a.val(moment(t).format("YYYY-MM-DD"))}});t(this).data("pikaday",o)})}}(jQuery),function(t){t.fn.imagefield=function(){return this.each(function(){var e=t(this);if(e.data("imagefield"))return!0;e.data("imagefield",!0);var a=e.find("select"),n=e.find(".input-preview figure"),r=n.parent("a");a.on("keydown change",function(){var t=a.find("option:selected"),e=t.data("url"),i=t.data("thumb");""===t.val()&&(e="#"),i?n.attr("style","background-image: url("+i+")"):n.attr("style","background-image: none"),r.attr("href",e)}).trigger("change"),e.find(".input-preview").on("click",function(){return"#"==t(this).attr("href")?!1:void 0}),e.find(".input").droppable({hoverClass:"over",accept:t(".sidebar .draggable-file"),drop:function(e,a){t(this).find("select").val(a.draggable.data("helper")).trigger("change")}})})}}(jQuery),function(t){var e=function(e){var a=t(e),n=a.data("style"),r=a.data("api"),i=a.data("sortable"),o="table"==n?a.find(".structure-table tbody"):a.find(".structure-entries");return i===!1?!1:void o.sortable({helper:function(e,a){return a.children().each(function(){t(this).width(t(this).width())}),a.addClass("structure-sortable-helper")},update:function(){var e=[];t.each(t(this).sortable("toArray"),function(t,a){e.push(a.replace("structure-entry-",""))}),t.post(r,{ids:e},function(){app.content.reload()})}})};t.fn.structure=function(){return this.each(function(){if(t(this).data("structure"))return t(this);var a=new e(this);return t(this).data("structure",a),t(this)})}}(jQuery),function(t){t.fn.counter=function(){return this.each(function(){var e=t(this);if(e.data("counter"))return e;var a=e.parent(".field").find(".input"),n=t.trim(a.val()).length,r=a.data("max"),i=a.data("min");a.keyup(function(){n=t.trim(a.val()).length,e.text(n+(r?"/"+r:"")),r&&n>r||i&&i>n?e.addClass("outside-range"):e.removeClass("outside-range")}).trigger("keyup"),e.data("counter",!0)})}}(jQuery),function(t){t.fn.editor=function(){return this.each(function(){if(t(this).data("editor"))return t(this);var e=t(this),a=e.parent().find(".field-buttons");e.autosize(),a.find(".btn").on("click.editorButton",function(a){e.focus();var n=t(this);if(n.data("action"))app.modal.open(n.data("action"),window.location.href);else{var r=e.getSelection(),i=n.data("tpl"),o=n.data("text");r.length>0&&(o=r);var u=i.replace("{text}",o);e.insertAtCursor(u),e.trigger("autosize.resize")}return!1}),a.find("[data-editor-shortcut]").each(function(a,n){var r=t(this).data("editor-shortcut"),i=function(e){return t(n).trigger("click"),!1};e.bind("keydown",r,i),r.match(/meta\+/)&&e.bind("keydown",r.replace("meta+","ctrl+"),i)}),e.data("editor",!0)})}}(jQuery),function(t){t.fn.urlfield=function(){return this.each(function(){var e=t(this);if(!e.data("urlfield")){e.data("urlfield",!0);var a=e.next(".field-icon");a.css({cursor:"pointer","pointer-events":"auto"}),a.on("click",function(){var a=t.trim(e.val());""!==a&&e.is(":valid")?window.open(a):e.focus()})}})}}(jQuery); \ No newline at end of file diff --git a/panel/assets/js/dist/panel.min.js b/panel/assets/js/dist/panel.min.js new file mode 100644 index 0000000..a21b1cc --- /dev/null +++ b/panel/assets/js/dist/panel.min.js @@ -0,0 +1,8 @@ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.moment=e()}(this,function(){"use strict";function t(){return Hn.apply(null,arguments)}function e(t){Hn=t}function n(t){return"[object Array]"===Object.prototype.toString.call(t)}function i(t){return t instanceof Date||"[object Date]"===Object.prototype.toString.call(t)}function o(t,e){var n,i=[];for(n=0;n0)for(n in Yn)i=Yn[n],o=e[i],"undefined"!=typeof o&&(t[i]=o);return t}function f(e){d(this,e),this._d=new Date(null!=e._d?e._d.getTime():NaN),Ln===!1&&(Ln=!0,t.updateOffset(this),Ln=!1)}function p(t){return t instanceof f||null!=t&&null!=t._isAMomentObject}function m(t){return 0>t?Math.ceil(t):Math.floor(t)}function g(t){var e=+t,n=0;return 0!==e&&isFinite(e)&&(n=m(e)),n}function v(t,e,n){var i,o=Math.min(t.length,e.length),r=Math.abs(t.length-e.length),s=0;for(i=0;o>i;i++)(n&&t[i]!==e[i]||!n&&g(t[i])!==g(e[i]))&&s++;return s+r}function y(){}function b(t){return t?t.toLowerCase().replace("_","-"):t}function _(t){for(var e,n,i,o,r=0;r0;){if(i=w(o.slice(0,e).join("-")))return i;if(n&&n.length>=e&&v(o,n,!0)>=e-1)break;e--}r++}return null}function w(t){var e=null;if(!jn[t]&&"undefined"!=typeof module&&module&&module.exports)try{e=An._abbr,require("./locale/"+t),x(e)}catch(n){}return jn[t]}function x(t,e){var n;return t&&(n="undefined"==typeof e?D(t):k(t,e),n&&(An=n)),An._abbr}function k(t,e){return null!==e?(e.abbr=t,jn[t]=jn[t]||new y,jn[t].set(e),x(t),jn[t]):(delete jn[t],null)}function D(t){var e;if(t&&t._locale&&t._locale._abbr&&(t=t._locale._abbr),!t)return An;if(!n(t)){if(e=w(t))return e;t=[t]}return _(t)}function C(t,e){var n=t.toLowerCase();Wn[n]=Wn[n+"s"]=Wn[e]=t}function T(t){return"string"==typeof t?Wn[t]||Wn[t.toLowerCase()]:void 0}function S(t){var e,n,i={};for(n in t)r(t,n)&&(e=T(n),e&&(i[e]=t[n]));return i}function P(e,n){return function(i){return null!=i?(N(this,e,i),t.updateOffset(this,n),this):M(this,e)}}function M(t,e){return t._d["get"+(t._isUTC?"UTC":"")+e]()}function N(t,e,n){return t._d["set"+(t._isUTC?"UTC":"")+e](n)}function E(t,e){var n;if("object"==typeof t)for(n in t)this.set(n,t[n]);else if(t=T(t),"function"==typeof this[t])return this[t](e);return this}function O(t,e,n){var i=""+Math.abs(t),o=e-i.length,r=t>=0;return(r?n?"+":"":"-")+Math.pow(10,Math.max(0,o)).toString().substr(1)+i}function I(t,e,n,i){var o=i;"string"==typeof i&&(o=function(){return this[i]()}),t&&(qn[t]=o),e&&(qn[e[0]]=function(){return O(o.apply(this,arguments),e[1],e[2])}),n&&(qn[n]=function(){return this.localeData().ordinal(o.apply(this,arguments),t)})}function H(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function A(t){var e,n,i=t.match(Fn);for(e=0,n=i.length;n>e;e++)qn[i[e]]?i[e]=qn[i[e]]:i[e]=H(i[e]);return function(o){var r="";for(e=0;n>e;e++)r+=i[e]instanceof Function?i[e].call(o,t):i[e];return r}}function Y(t,e){return t.isValid()?(e=L(e,t.localeData()),zn[e]=zn[e]||A(e),zn[e](t)):t.localeData().invalidDate()}function L(t,e){function n(t){return e.longDateFormat(t)||t}var i=5;for(Rn.lastIndex=0;i>=0&&Rn.test(t);)t=t.replace(Rn,n),Rn.lastIndex=0,i-=1;return t}function j(t){return"function"==typeof t&&"[object Function]"===Object.prototype.toString.call(t)}function W(t,e,n){oi[t]=j(e)?e:function(t){return t&&n?n:e}}function F(t,e){return r(oi,t)?oi[t](e._strict,e._locale):new RegExp(R(t))}function R(t){return t.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,n,i,o){return e||n||i||o}).replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function z(t,e){var n,i=e;for("string"==typeof t&&(t=[t]),"number"==typeof e&&(i=function(t,n){n[e]=g(t)}),n=0;ni;i++){if(o=a([2e3,i]),n&&!this._longMonthsParse[i]&&(this._longMonthsParse[i]=new RegExp("^"+this.months(o,"").replace(".","")+"$","i"),this._shortMonthsParse[i]=new RegExp("^"+this.monthsShort(o,"").replace(".","")+"$","i")),n||this._monthsParse[i]||(r="^"+this.months(o,"")+"|^"+this.monthsShort(o,""),this._monthsParse[i]=new RegExp(r.replace(".",""),"i")),n&&"MMMM"===e&&this._longMonthsParse[i].test(t))return i;if(n&&"MMM"===e&&this._shortMonthsParse[i].test(t))return i;if(!n&&this._monthsParse[i].test(t))return i}}function Q(t,e){var n;return"string"==typeof e&&(e=t.localeData().monthsParse(e),"number"!=typeof e)?t:(n=Math.min(t.date(),U(t.year(),e)),t._d["set"+(t._isUTC?"UTC":"")+"Month"](e,n),t)}function V(e){return null!=e?(Q(this,e),t.updateOffset(this,!0),this):M(this,"Month")}function Z(){return U(this.year(),this.month())}function J(t){var e,n=t._a;return n&&-2===c(t).overflow&&(e=n[ai]<0||n[ai]>11?ai:n[li]<1||n[li]>U(n[si],n[ai])?li:n[ci]<0||n[ci]>24||24===n[ci]&&(0!==n[ui]||0!==n[hi]||0!==n[di])?ci:n[ui]<0||n[ui]>59?ui:n[hi]<0||n[hi]>59?hi:n[di]<0||n[di]>999?di:-1,c(t)._overflowDayOfYear&&(si>e||e>li)&&(e=li),c(t).overflow=e),t}function K(e){t.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+e)}function tt(t,e){var n=!0;return s(function(){return n&&(K(t+"\n"+(new Error).stack),n=!1),e.apply(this,arguments)},e)}function et(t,e){mi[t]||(K(e),mi[t]=!0)}function nt(t){var e,n,i=t._i,o=gi.exec(i);if(o){for(c(t).iso=!0,e=0,n=vi.length;n>e;e++)if(vi[e][1].exec(i)){t._f=vi[e][0];break}for(e=0,n=yi.length;n>e;e++)if(yi[e][1].exec(i)){t._f+=(o[6]||" ")+yi[e][0];break}i.match(ei)&&(t._f+="Z"),wt(t)}else t._isValid=!1}function it(e){var n=bi.exec(e._i);return null!==n?void(e._d=new Date(+n[1])):(nt(e),void(e._isValid===!1&&(delete e._isValid,t.createFromInputFallback(e))))}function ot(t,e,n,i,o,r,s){var a=new Date(t,e,n,i,o,r,s);return 1970>t&&a.setFullYear(t),a}function rt(t){var e=new Date(Date.UTC.apply(null,arguments));return 1970>t&&e.setUTCFullYear(t),e}function st(t){return at(t)?366:365}function at(t){return t%4===0&&t%100!==0||t%400===0}function lt(){return at(this.year())}function ct(t,e,n){var i,o=n-e,r=n-t.day();return r>o&&(r-=7),o-7>r&&(r+=7),i=Mt(t).add(r,"d"),{week:Math.ceil(i.dayOfYear()/7),year:i.year()}}function ut(t){return ct(t,this._week.dow,this._week.doy).week}function ht(){return this._week.dow}function dt(){return this._week.doy}function ft(t){var e=this.localeData().week(this);return null==t?e:this.add(7*(t-e),"d")}function pt(t){var e=ct(this,1,4).week;return null==t?e:this.add(7*(t-e),"d")}function mt(t,e,n,i,o){var r,s=6+o-i,a=rt(t,0,1+s),l=a.getUTCDay();return o>l&&(l+=7),n=null!=n?1*n:o,r=1+s+7*(e-1)-l+n,{year:r>0?t:t-1,dayOfYear:r>0?r:st(t-1)+r}}function gt(t){var e=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==t?e:this.add(t-e,"d")}function vt(t,e,n){return null!=t?t:null!=e?e:n}function yt(t){var e=new Date;return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}function bt(t){var e,n,i,o,r=[];if(!t._d){for(i=yt(t),t._w&&null==t._a[li]&&null==t._a[ai]&&_t(t),t._dayOfYear&&(o=vt(t._a[si],i[si]),t._dayOfYear>st(o)&&(c(t)._overflowDayOfYear=!0),n=rt(o,0,t._dayOfYear),t._a[ai]=n.getUTCMonth(),t._a[li]=n.getUTCDate()),e=0;3>e&&null==t._a[e];++e)t._a[e]=r[e]=i[e];for(;7>e;e++)t._a[e]=r[e]=null==t._a[e]?2===e?1:0:t._a[e];24===t._a[ci]&&0===t._a[ui]&&0===t._a[hi]&&0===t._a[di]&&(t._nextDay=!0,t._a[ci]=0),t._d=(t._useUTC?rt:ot).apply(null,r),null!=t._tzm&&t._d.setUTCMinutes(t._d.getUTCMinutes()-t._tzm),t._nextDay&&(t._a[ci]=24)}}function _t(t){var e,n,i,o,r,s,a;e=t._w,null!=e.GG||null!=e.W||null!=e.E?(r=1,s=4,n=vt(e.GG,t._a[si],ct(Mt(),1,4).year),i=vt(e.W,1),o=vt(e.E,1)):(r=t._locale._week.dow,s=t._locale._week.doy,n=vt(e.gg,t._a[si],ct(Mt(),r,s).year),i=vt(e.w,1),null!=e.d?(o=e.d,r>o&&++i):o=null!=e.e?e.e+r:r),a=mt(n,i,o,s,r),t._a[si]=a.year,t._dayOfYear=a.dayOfYear}function wt(e){if(e._f===t.ISO_8601)return void nt(e);e._a=[],c(e).empty=!0;var n,i,o,r,s,a=""+e._i,l=a.length,u=0;for(o=L(e._f,e._locale).match(Fn)||[],n=0;n0&&c(e).unusedInput.push(s),a=a.slice(a.indexOf(i)+i.length),u+=i.length),qn[r]?(i?c(e).empty=!1:c(e).unusedTokens.push(r),$(r,i,e)):e._strict&&!i&&c(e).unusedTokens.push(r);c(e).charsLeftOver=l-u,a.length>0&&c(e).unusedInput.push(a),c(e).bigHour===!0&&e._a[ci]<=12&&e._a[ci]>0&&(c(e).bigHour=void 0),e._a[ci]=xt(e._locale,e._a[ci],e._meridiem),bt(e),J(e)}function xt(t,e,n){var i;return null==n?e:null!=t.meridiemHour?t.meridiemHour(e,n):null!=t.isPM?(i=t.isPM(n),i&&12>e&&(e+=12),i||12!==e||(e=0),e):e}function kt(t){var e,n,i,o,r;if(0===t._f.length)return c(t).invalidFormat=!0,void(t._d=new Date(NaN));for(o=0;or)&&(i=r,n=e));s(t,n||e)}function Dt(t){if(!t._d){var e=S(t._i);t._a=[e.year,e.month,e.day||e.date,e.hour,e.minute,e.second,e.millisecond],bt(t)}}function Ct(t){var e=new f(J(Tt(t)));return e._nextDay&&(e.add(1,"d"),e._nextDay=void 0),e}function Tt(t){var e=t._i,o=t._f;return t._locale=t._locale||D(t._l),null===e||void 0===o&&""===e?h({nullInput:!0}):("string"==typeof e&&(t._i=e=t._locale.preparse(e)),p(e)?new f(J(e)):(n(o)?kt(t):o?wt(t):i(e)?t._d=e:St(t),t))}function St(e){var r=e._i;void 0===r?e._d=new Date:i(r)?e._d=new Date(+r):"string"==typeof r?it(e):n(r)?(e._a=o(r.slice(0),function(t){return parseInt(t,10)}),bt(e)):"object"==typeof r?Dt(e):"number"==typeof r?e._d=new Date(r):t.createFromInputFallback(e)}function Pt(t,e,n,i,o){var r={};return"boolean"==typeof n&&(i=n,n=void 0),r._isAMomentObject=!0,r._useUTC=r._isUTC=o,r._l=n,r._i=t,r._f=e,r._strict=i,Ct(r)}function Mt(t,e,n,i){return Pt(t,e,n,i,!1)}function Nt(t,e){var i,o;if(1===e.length&&n(e[0])&&(e=e[0]),!e.length)return Mt();for(i=e[0],o=1;ot&&(t=-t,n="-"),n+O(~~(t/60),2)+e+O(~~t%60,2)})}function Yt(t){var e=(t||"").match(ei)||[],n=e[e.length-1]||[],i=(n+"").match(Di)||["-",0,0],o=+(60*i[1])+g(i[2]);return"+"===i[0]?o:-o}function Lt(e,n){var o,r;return n._isUTC?(o=n.clone(),r=(p(e)||i(e)?+e:+Mt(e))-+o,o._d.setTime(+o._d+r),t.updateOffset(o,!1),o):Mt(e).local()}function jt(t){return 15*-Math.round(t._d.getTimezoneOffset()/15)}function Wt(e,n){var i,o=this._offset||0;return null!=e?("string"==typeof e&&(e=Yt(e)),Math.abs(e)<16&&(e=60*e),!this._isUTC&&n&&(i=jt(this)),this._offset=e,this._isUTC=!0,null!=i&&this.add(i,"m"),o!==e&&(!n||this._changeInProgress?ee(this,Vt(e-o,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,t.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?o:jt(this)}function Ft(t,e){return null!=t?("string"!=typeof t&&(t=-t),this.utcOffset(t,e),this):-this.utcOffset()}function Rt(t){return this.utcOffset(0,t)}function zt(t){return this._isUTC&&(this.utcOffset(0,t),this._isUTC=!1,t&&this.subtract(jt(this),"m")),this}function qt(){return this._tzm?this.utcOffset(this._tzm):"string"==typeof this._i&&this.utcOffset(Yt(this._i)),this}function $t(t){return t=t?Mt(t).utcOffset():0,(this.utcOffset()-t)%60===0}function Ut(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Bt(){if("undefined"!=typeof this._isDSTShifted)return this._isDSTShifted;var t={};if(d(t,this),t=Tt(t),t._a){var e=t._isUTC?a(t._a):Mt(t._a);this._isDSTShifted=this.isValid()&&v(t._a,e.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function Xt(){return!this._isUTC}function Gt(){return this._isUTC}function Qt(){return this._isUTC&&0===this._offset}function Vt(t,e){var n,i,o,s=t,a=null;return Ht(t)?s={ms:t._milliseconds,d:t._days,M:t._months}:"number"==typeof t?(s={},e?s[e]=t:s.milliseconds=t):(a=Ci.exec(t))?(n="-"===a[1]?-1:1,s={y:0,d:g(a[li])*n,h:g(a[ci])*n,m:g(a[ui])*n,s:g(a[hi])*n,ms:g(a[di])*n}):(a=Ti.exec(t))?(n="-"===a[1]?-1:1,s={y:Zt(a[2],n),M:Zt(a[3],n),d:Zt(a[4],n),h:Zt(a[5],n),m:Zt(a[6],n),s:Zt(a[7],n),w:Zt(a[8],n)}):null==s?s={}:"object"==typeof s&&("from"in s||"to"in s)&&(o=Kt(Mt(s.from),Mt(s.to)),s={},s.ms=o.milliseconds,s.M=o.months),i=new It(s),Ht(t)&&r(t,"_locale")&&(i._locale=t._locale),i}function Zt(t,e){var n=t&&parseFloat(t.replace(",","."));return(isNaN(n)?0:n)*e}function Jt(t,e){var n={milliseconds:0,months:0};return n.months=e.month()-t.month()+12*(e.year()-t.year()),t.clone().add(n.months,"M").isAfter(e)&&--n.months,n.milliseconds=+e-+t.clone().add(n.months,"M"),n}function Kt(t,e){var n;return e=Lt(e,t),t.isBefore(e)?n=Jt(t,e):(n=Jt(e,t),n.milliseconds=-n.milliseconds,n.months=-n.months),n}function te(t,e){return function(n,i){var o,r;return null===i||isNaN(+i)||(et(e,"moment()."+e+"(period, number) is deprecated. Please use moment()."+e+"(number, period)."),r=n,n=i,i=r),n="string"==typeof n?+n:n,o=Vt(n,i),ee(this,o,t),this}}function ee(e,n,i,o){var r=n._milliseconds,s=n._days,a=n._months;o=null==o?!0:o,r&&e._d.setTime(+e._d+r*i),s&&N(e,"Date",M(e,"Date")+s*i),a&&Q(e,M(e,"Month")+a*i),o&&t.updateOffset(e,s||a)}function ne(t,e){var n=t||Mt(),i=Lt(n,this).startOf("day"),o=this.diff(i,"days",!0),r=-6>o?"sameElse":-1>o?"lastWeek":0>o?"lastDay":1>o?"sameDay":2>o?"nextDay":7>o?"nextWeek":"sameElse";return this.format(e&&e[r]||this.localeData().calendar(r,this,Mt(n)))}function ie(){return new f(this)}function oe(t,e){var n;return e=T("undefined"!=typeof e?e:"millisecond"),"millisecond"===e?(t=p(t)?t:Mt(t),+this>+t):(n=p(t)?+t:+Mt(t),n<+this.clone().startOf(e))}function re(t,e){var n;return e=T("undefined"!=typeof e?e:"millisecond"),"millisecond"===e?(t=p(t)?t:Mt(t),+t>+this):(n=p(t)?+t:+Mt(t),+this.clone().endOf(e)e-r?(n=t.clone().add(o-1,"months"),i=(e-r)/(r-n)):(n=t.clone().add(o+1,"months"),i=(e-r)/(n-r)),-(o+i)}function ue(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function he(){var t=this.clone().utc();return 0e;e++)if(this._weekdaysParse[e]||(n=Mt([2e3,1]).day(e),i="^"+this.weekdays(n,"")+"|^"+this.weekdaysShort(n,"")+"|^"+this.weekdaysMin(n,""),this._weekdaysParse[e]=new RegExp(i.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e}function Re(t){var e=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=t?(t=Ye(t,this.localeData()),this.add(t-e,"d")):e}function ze(t){var e=(this.day()+7-this.localeData()._week.dow)%7;return null==t?e:this.add(t-e,"d")}function qe(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)}function $e(t,e){I(t,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),e)})}function Ue(t,e){return e._meridiemParse}function Be(t){return"p"===(t+"").toLowerCase().charAt(0)}function Xe(t,e,n){return t>11?n?"pm":"PM":n?"am":"AM"}function Ge(t,e){e[di]=g(1e3*("0."+t))}function Qe(){return this._isUTC?"UTC":""}function Ve(){return this._isUTC?"Coordinated Universal Time":""}function Ze(t){return Mt(1e3*t)}function Je(){return Mt.apply(null,arguments).parseZone()}function Ke(t,e,n){var i=this._calendar[t];return"function"==typeof i?i.call(e,n):i}function tn(t){var e=this._longDateFormat[t],n=this._longDateFormat[t.toUpperCase()];return e||!n?e:(this._longDateFormat[t]=n.replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t])}function en(){return this._invalidDate}function nn(t){return this._ordinal.replace("%d",t)}function on(t){return t}function rn(t,e,n,i){var o=this._relativeTime[n];return"function"==typeof o?o(t,e,n,i):o.replace(/%d/i,t)}function sn(t,e){var n=this._relativeTime[t>0?"future":"past"];return"function"==typeof n?n(e):n.replace(/%s/i,e)}function an(t){var e,n;for(n in t)e=t[n],"function"==typeof e?this[n]=e:this["_"+n]=e;this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)}function ln(t,e,n,i){var o=D(),r=a().set(i,e);return o[n](r,t)}function cn(t,e,n,i,o){if("number"==typeof t&&(e=t,t=void 0),t=t||"",null!=e)return ln(t,e,n,o);var r,s=[];for(r=0;i>r;r++)s[r]=ln(t,r,n,o);return s}function un(t,e){return cn(t,e,"months",12,"month")}function hn(t,e){return cn(t,e,"monthsShort",12,"month")}function dn(t,e){return cn(t,e,"weekdays",7,"day")}function fn(t,e){return cn(t,e,"weekdaysShort",7,"day")}function pn(t,e){return cn(t,e,"weekdaysMin",7,"day")}function mn(){var t=this._data;return this._milliseconds=Qi(this._milliseconds),this._days=Qi(this._days),this._months=Qi(this._months),t.milliseconds=Qi(t.milliseconds),t.seconds=Qi(t.seconds),t.minutes=Qi(t.minutes),t.hours=Qi(t.hours),t.months=Qi(t.months),t.years=Qi(t.years),this}function gn(t,e,n,i){var o=Vt(e,n);return t._milliseconds+=i*o._milliseconds,t._days+=i*o._days,t._months+=i*o._months,t._bubble()}function vn(t,e){return gn(this,t,e,1)}function yn(t,e){return gn(this,t,e,-1)}function bn(t){return 0>t?Math.floor(t):Math.ceil(t)}function _n(){var t,e,n,i,o,r=this._milliseconds,s=this._days,a=this._months,l=this._data;return r>=0&&s>=0&&a>=0||0>=r&&0>=s&&0>=a||(r+=864e5*bn(xn(a)+s),s=0,a=0),l.milliseconds=r%1e3,t=m(r/1e3),l.seconds=t%60,e=m(t/60),l.minutes=e%60,n=m(e/60),l.hours=n%24,s+=m(n/24),o=m(wn(s)),a+=o,s-=bn(xn(o)),i=m(a/12),a%=12,l.days=s,l.months=a,l.years=i,this}function wn(t){return 4800*t/146097}function xn(t){return 146097*t/4800}function kn(t){var e,n,i=this._milliseconds;if(t=T(t),"month"===t||"year"===t)return e=this._days+i/864e5,n=this._months+wn(e),"month"===t?n:n/12;switch(e=this._days+Math.round(xn(this._months)),t){case"week":return e/7+i/6048e5;case"day":return e+i/864e5;case"hour":return 24*e+i/36e5;case"minute":return 1440*e+i/6e4;case"second":return 86400*e+i/1e3;case"millisecond":return Math.floor(864e5*e)+i;default:throw new Error("Unknown unit "+t)}}function Dn(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*g(this._months/12)}function Cn(t){return function(){return this.as(t)}}function Tn(t){return t=T(t),this[t+"s"]()}function Sn(t){return function(){return this._data[t]}}function Pn(){return m(this.days()/7)}function Mn(t,e,n,i,o){return o.relativeTime(e||1,!!n,t,i)}function Nn(t,e,n){var i=Vt(t).abs(),o=ho(i.as("s")),r=ho(i.as("m")),s=ho(i.as("h")),a=ho(i.as("d")),l=ho(i.as("M")),c=ho(i.as("y")),u=o0,u[4]=n,Mn.apply(null,u)}function En(t,e){return void 0===fo[t]?!1:void 0===e?fo[t]:(fo[t]=e,!0)}function On(t){var e=this.localeData(),n=Nn(this,!t,e);return t&&(n=e.pastFuture(+this,n)),e.postformat(n)}function In(){var t,e,n,i=po(this._milliseconds)/1e3,o=po(this._days),r=po(this._months);t=m(i/60),e=m(t/60),i%=60,t%=60,n=m(r/12),r%=12;var s=n,a=r,l=o,c=e,u=t,h=i,d=this.asSeconds();return d?(0>d?"-":"")+"P"+(s?s+"Y":"")+(a?a+"M":"")+(l?l+"D":"")+(c||u||h?"T":"")+(c?c+"H":"")+(u?u+"M":"")+(h?h+"S":""):"P0D"}var Hn,An,Yn=t.momentProperties=[],Ln=!1,jn={},Wn={},Fn=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,Rn=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,zn={},qn={},$n=/\d/,Un=/\d\d/,Bn=/\d{3}/,Xn=/\d{4}/,Gn=/[+-]?\d{6}/,Qn=/\d\d?/,Vn=/\d{1,3}/,Zn=/\d{1,4}/,Jn=/[+-]?\d{1,6}/,Kn=/\d+/,ti=/[+-]?\d+/,ei=/Z|[+-]\d\d:?\d\d/gi,ni=/[+-]?\d+(\.\d{1,3})?/,ii=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,oi={},ri={},si=0,ai=1,li=2,ci=3,ui=4,hi=5,di=6;I("M",["MM",2],"Mo",function(){return this.month()+1}),I("MMM",0,0,function(t){return this.localeData().monthsShort(this,t)}),I("MMMM",0,0,function(t){return this.localeData().months(this,t)}),C("month","M"),W("M",Qn),W("MM",Qn,Un),W("MMM",ii),W("MMMM",ii),z(["M","MM"],function(t,e){e[ai]=g(t)-1}),z(["MMM","MMMM"],function(t,e,n,i){var o=n._locale.monthsParse(t,i,n._strict);null!=o?e[ai]=o:c(n).invalidMonth=t});var fi="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),pi="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),mi={};t.suppressDeprecationWarnings=!1;var gi=/^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,vi=[["YYYYYY-MM-DD",/[+-]\d{6}-\d{2}-\d{2}/],["YYYY-MM-DD",/\d{4}-\d{2}-\d{2}/],["GGGG-[W]WW-E",/\d{4}-W\d{2}-\d/],["GGGG-[W]WW",/\d{4}-W\d{2}/],["YYYY-DDD",/\d{4}-\d{3}/]],yi=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],bi=/^\/?Date\((\-?\d+)/i;t.createFromInputFallback=tt("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(t){t._d=new Date(t._i+(t._useUTC?" UTC":""))}),I(0,["YY",2],0,function(){return this.year()%100}),I(0,["YYYY",4],0,"year"),I(0,["YYYYY",5],0,"year"),I(0,["YYYYYY",6,!0],0,"year"),C("year","y"),W("Y",ti),W("YY",Qn,Un),W("YYYY",Zn,Xn),W("YYYYY",Jn,Gn),W("YYYYYY",Jn,Gn),z(["YYYYY","YYYYYY"],si),z("YYYY",function(e,n){n[si]=2===e.length?t.parseTwoDigitYear(e):g(e)}),z("YY",function(e,n){n[si]=t.parseTwoDigitYear(e)}),t.parseTwoDigitYear=function(t){return g(t)+(g(t)>68?1900:2e3)};var _i=P("FullYear",!1);I("w",["ww",2],"wo","week"),I("W",["WW",2],"Wo","isoWeek"),C("week","w"),C("isoWeek","W"),W("w",Qn),W("ww",Qn,Un),W("W",Qn),W("WW",Qn,Un),q(["w","ww","W","WW"],function(t,e,n,i){e[i.substr(0,1)]=g(t)});var wi={dow:0,doy:6};I("DDD",["DDDD",3],"DDDo","dayOfYear"),C("dayOfYear","DDD"),W("DDD",Vn),W("DDDD",Bn),z(["DDD","DDDD"],function(t,e,n){n._dayOfYear=g(t)}),t.ISO_8601=function(){};var xi=tt("moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(){var t=Mt.apply(null,arguments);return this>t?this:t}),ki=tt("moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(){var t=Mt.apply(null,arguments);return t>this?this:t});At("Z",":"),At("ZZ",""),W("Z",ei),W("ZZ",ei),z(["Z","ZZ"],function(t,e,n){n._useUTC=!0,n._tzm=Yt(t)});var Di=/([\+\-]|\d\d)/gi;t.updateOffset=function(){};var Ci=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,Ti=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/;Vt.fn=It.prototype;var Si=te(1,"add"),Pi=te(-1,"subtract");t.defaultFormat="YYYY-MM-DDTHH:mm:ssZ";var Mi=tt("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(t){return void 0===t?this.localeData():this.locale(t)});I(0,["gg",2],0,function(){return this.weekYear()%100}),I(0,["GG",2],0,function(){return this.isoWeekYear()%100}),Me("gggg","weekYear"),Me("ggggg","weekYear"),Me("GGGG","isoWeekYear"),Me("GGGGG","isoWeekYear"),C("weekYear","gg"),C("isoWeekYear","GG"),W("G",ti),W("g",ti),W("GG",Qn,Un),W("gg",Qn,Un),W("GGGG",Zn,Xn),W("gggg",Zn,Xn),W("GGGGG",Jn,Gn),W("ggggg",Jn,Gn),q(["gggg","ggggg","GGGG","GGGGG"],function(t,e,n,i){e[i.substr(0,2)]=g(t)}),q(["gg","GG"],function(e,n,i,o){n[o]=t.parseTwoDigitYear(e)}),I("Q",0,0,"quarter"),C("quarter","Q"),W("Q",$n),z("Q",function(t,e){e[ai]=3*(g(t)-1)}),I("D",["DD",2],"Do","date"),C("date","D"),W("D",Qn),W("DD",Qn,Un),W("Do",function(t,e){return t?e._ordinalParse:e._ordinalParseLenient}),z(["D","DD"],li),z("Do",function(t,e){e[li]=g(t.match(Qn)[0],10)});var Ni=P("Date",!0);I("d",0,"do","day"),I("dd",0,0,function(t){return this.localeData().weekdaysMin(this,t)}),I("ddd",0,0,function(t){return this.localeData().weekdaysShort(this,t)}),I("dddd",0,0,function(t){return this.localeData().weekdays(this,t)}),I("e",0,0,"weekday"),I("E",0,0,"isoWeekday"),C("day","d"),C("weekday","e"),C("isoWeekday","E"),W("d",Qn),W("e",Qn),W("E",Qn),W("dd",ii),W("ddd",ii),W("dddd",ii),q(["dd","ddd","dddd"],function(t,e,n){var i=n._locale.weekdaysParse(t);null!=i?e.d=i:c(n).invalidWeekday=t}),q(["d","e","E"],function(t,e,n,i){e[i]=g(t)});var Ei="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),Oi="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),Ii="Su_Mo_Tu_We_Th_Fr_Sa".split("_");I("H",["HH",2],0,"hour"),I("h",["hh",2],0,function(){return this.hours()%12||12}),$e("a",!0),$e("A",!1),C("hour","h"),W("a",Ue),W("A",Ue),W("H",Qn),W("h",Qn),W("HH",Qn,Un),W("hh",Qn,Un),z(["H","HH"],ci),z(["a","A"],function(t,e,n){n._isPm=n._locale.isPM(t),n._meridiem=t}),z(["h","hh"],function(t,e,n){e[ci]=g(t),c(n).bigHour=!0});var Hi=/[ap]\.?m?\.?/i,Ai=P("Hours",!0);I("m",["mm",2],0,"minute"),C("minute","m"),W("m",Qn),W("mm",Qn,Un),z(["m","mm"],ui);var Yi=P("Minutes",!1);I("s",["ss",2],0,"second"),C("second","s"),W("s",Qn),W("ss",Qn,Un),z(["s","ss"],hi);var Li=P("Seconds",!1);I("S",0,0,function(){return~~(this.millisecond()/100)}),I(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),I(0,["SSS",3],0,"millisecond"),I(0,["SSSS",4],0,function(){return 10*this.millisecond()}),I(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),I(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),I(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),I(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),I(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),C("millisecond","ms"),W("S",Vn,$n),W("SS",Vn,Un),W("SSS",Vn,Bn);var ji;for(ji="SSSS";ji.length<=9;ji+="S")W(ji,Kn);for(ji="S";ji.length<=9;ji+="S")z(ji,Ge);var Wi=P("Milliseconds",!1);I("z",0,0,"zoneAbbr"),I("zz",0,0,"zoneName");var Fi=f.prototype;Fi.add=Si,Fi.calendar=ne,Fi.clone=ie,Fi.diff=le,Fi.endOf=_e,Fi.format=de,Fi.from=fe,Fi.fromNow=pe,Fi.to=me,Fi.toNow=ge,Fi.get=E,Fi.invalidAt=Pe,Fi.isAfter=oe,Fi.isBefore=re,Fi.isBetween=se,Fi.isSame=ae,Fi.isValid=Te,Fi.lang=Mi,Fi.locale=ve,Fi.localeData=ye,Fi.max=ki,Fi.min=xi,Fi.parsingFlags=Se,Fi.set=E,Fi.startOf=be,Fi.subtract=Pi,Fi.toArray=De,Fi.toObject=Ce,Fi.toDate=ke,Fi.toISOString=he,Fi.toJSON=he,Fi.toString=ue,Fi.unix=xe,Fi.valueOf=we,Fi.year=_i,Fi.isLeapYear=lt,Fi.weekYear=Ee,Fi.isoWeekYear=Oe,Fi.quarter=Fi.quarters=Ae,Fi.month=V,Fi.daysInMonth=Z,Fi.week=Fi.weeks=ft,Fi.isoWeek=Fi.isoWeeks=pt,Fi.weeksInYear=He,Fi.isoWeeksInYear=Ie,Fi.date=Ni,Fi.day=Fi.days=Re,Fi.weekday=ze,Fi.isoWeekday=qe,Fi.dayOfYear=gt,Fi.hour=Fi.hours=Ai,Fi.minute=Fi.minutes=Yi,Fi.second=Fi.seconds=Li, +Fi.millisecond=Fi.milliseconds=Wi,Fi.utcOffset=Wt,Fi.utc=Rt,Fi.local=zt,Fi.parseZone=qt,Fi.hasAlignedHourOffset=$t,Fi.isDST=Ut,Fi.isDSTShifted=Bt,Fi.isLocal=Xt,Fi.isUtcOffset=Gt,Fi.isUtc=Qt,Fi.isUTC=Qt,Fi.zoneAbbr=Qe,Fi.zoneName=Ve,Fi.dates=tt("dates accessor is deprecated. Use date instead.",Ni),Fi.months=tt("months accessor is deprecated. Use month instead",V),Fi.years=tt("years accessor is deprecated. Use year instead",_i),Fi.zone=tt("moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779",Ft);var Ri=Fi,zi={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},qi={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},$i="Invalid date",Ui="%d",Bi=/\d{1,2}/,Xi={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},Gi=y.prototype;Gi._calendar=zi,Gi.calendar=Ke,Gi._longDateFormat=qi,Gi.longDateFormat=tn,Gi._invalidDate=$i,Gi.invalidDate=en,Gi._ordinal=Ui,Gi.ordinal=nn,Gi._ordinalParse=Bi,Gi.preparse=on,Gi.postformat=on,Gi._relativeTime=Xi,Gi.relativeTime=rn,Gi.pastFuture=sn,Gi.set=an,Gi.months=B,Gi._months=fi,Gi.monthsShort=X,Gi._monthsShort=pi,Gi.monthsParse=G,Gi.week=ut,Gi._week=wi,Gi.firstDayOfYear=dt,Gi.firstDayOfWeek=ht,Gi.weekdays=Le,Gi._weekdays=Ei,Gi.weekdaysMin=We,Gi._weekdaysMin=Ii,Gi.weekdaysShort=je,Gi._weekdaysShort=Oi,Gi.weekdaysParse=Fe,Gi.isPM=Be,Gi._meridiemParse=Hi,Gi.meridiem=Xe,x("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(t){var e=t%10,n=1===g(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th";return t+n}}),t.lang=tt("moment.lang is deprecated. Use moment.locale instead.",x),t.langData=tt("moment.langData is deprecated. Use moment.localeData instead.",D);var Qi=Math.abs,Vi=Cn("ms"),Zi=Cn("s"),Ji=Cn("m"),Ki=Cn("h"),to=Cn("d"),eo=Cn("w"),no=Cn("M"),io=Cn("y"),oo=Sn("milliseconds"),ro=Sn("seconds"),so=Sn("minutes"),ao=Sn("hours"),lo=Sn("days"),co=Sn("months"),uo=Sn("years"),ho=Math.round,fo={s:45,m:45,h:22,d:26,M:11},po=Math.abs,mo=It.prototype;mo.abs=mn,mo.add=vn,mo.subtract=yn,mo.as=kn,mo.asMilliseconds=Vi,mo.asSeconds=Zi,mo.asMinutes=Ji,mo.asHours=Ki,mo.asDays=to,mo.asWeeks=eo,mo.asMonths=no,mo.asYears=io,mo.valueOf=Dn,mo._bubble=_n,mo.get=Tn,mo.milliseconds=oo,mo.seconds=ro,mo.minutes=so,mo.hours=ao,mo.days=lo,mo.weeks=Pn,mo.months=co,mo.years=uo,mo.humanize=On,mo.toISOString=In,mo.toString=In,mo.toJSON=In,mo.locale=ve,mo.localeData=ye,mo.toIsoString=tt("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",In),mo.lang=Mi,I("X",0,0,"unix"),I("x",0,0,"valueOf"),W("x",ti),W("X",ni),z("X",function(t,e,n){n._d=new Date(1e3*parseFloat(t,10))}),z("x",function(t,e,n){n._d=new Date(g(t))}),t.version="2.10.6",e(Mt),t.fn=Ri,t.min=Et,t.max=Ot,t.utc=a,t.unix=Ze,t.months=un,t.isDate=i,t.locale=x,t.invalid=h,t.duration=Vt,t.isMoment=p,t.weekdays=dn,t.parseZone=Je,t.localeData=D,t.isDuration=Ht,t.monthsShort=hn,t.weekdaysMin=pn,t.defineLocale=k,t.weekdaysShort=fn,t.normalizeUnits=T,t.relativeTimeThreshold=En;var go=t;return go}),function(t,e){"function"==typeof define&&define.amd?define(e):"object"==typeof exports?module.exports=e():t.NProgress=e()}(this,function(){function t(t,e,n){return e>t?e:t>n?n:t}function e(t){return 100*(-1+t)}function n(t,n,i){var o;return o="translate3d"===c.positionUsing?{transform:"translate3d("+e(t)+"%,0,0)"}:"translate"===c.positionUsing?{transform:"translate("+e(t)+"%,0)"}:{"margin-left":e(t)+"%"},o.transition="all "+n+"ms "+i,o}function i(t,e){var n="string"==typeof t?t:s(t);return n.indexOf(" "+e+" ")>=0}function o(t,e){var n=s(t),o=n+e;i(n,e)||(t.className=o.substring(1))}function r(t,e){var n,o=s(t);i(t,e)&&(n=o.replace(" "+e+" "," "),t.className=n.substring(1,n.length-1))}function s(t){return(" "+(t.className||"")+" ").replace(/\s+/gi," ")}function a(t){t&&t.parentNode&&t.parentNode.removeChild(t)}var l={};l.version="0.2.0";var c=l.settings={minimum:.08,easing:"ease",positionUsing:"",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,showSpinner:!0,barSelector:'[role="bar"]',spinnerSelector:'[role="spinner"]',parent:"body",template:'

    '};l.configure=function(t){var e,n;for(e in t)n=t[e],void 0!==n&&t.hasOwnProperty(e)&&(c[e]=n);return this},l.status=null,l.set=function(e){var i=l.isStarted();e=t(e,c.minimum,1),l.status=1===e?null:e;var o=l.render(!i),r=o.querySelector(c.barSelector),s=c.speed,a=c.easing;return o.offsetWidth,u(function(t){""===c.positionUsing&&(c.positionUsing=l.getPositioningCSS()),h(r,n(e,s,a)),1===e?(h(o,{transition:"none",opacity:1}),o.offsetWidth,setTimeout(function(){h(o,{transition:"all "+s+"ms linear",opacity:0}),setTimeout(function(){l.remove(),t()},s)},s)):setTimeout(t,s)}),this},l.isStarted=function(){return"number"==typeof l.status},l.start=function(){l.status||l.set(0);var t=function(){setTimeout(function(){l.status&&(l.trickle(),t())},c.trickleSpeed)};return c.trickle&&t(),this},l.done=function(t){return t||l.status?l.inc(.3+.5*Math.random()).set(1):this},l.inc=function(e){var n=l.status;return n?("number"!=typeof e&&(e=(1-n)*t(Math.random()*n,.1,.95)),n=t(n+e,0,.994),l.set(n)):l.start()},l.trickle=function(){return l.inc(Math.random()*c.trickleRate)},function(){var t=0,e=0;l.promise=function(n){return n&&"resolved"!==n.state()?(0===e&&l.start(),t++,e++,n.always(function(){e--,0===e?(t=0,l.done()):l.set((t-e)/t)}),this):this}}(),l.render=function(t){if(l.isRendered())return document.getElementById("nprogress");o(document.documentElement,"nprogress-busy");var n=document.createElement("div");n.id="nprogress",n.innerHTML=c.template;var i,r=n.querySelector(c.barSelector),s=t?"-100":e(l.status||0),u=document.querySelector(c.parent);return h(r,{transition:"all 0 linear",transform:"translate3d("+s+"%,0,0)"}),c.showSpinner||(i=n.querySelector(c.spinnerSelector),i&&a(i)),u!=document.body&&o(u,"nprogress-custom-parent"),u.appendChild(n),n},l.remove=function(){r(document.documentElement,"nprogress-busy"),r(document.querySelector(c.parent),"nprogress-custom-parent");var t=document.getElementById("nprogress");t&&a(t)},l.isRendered=function(){return!!document.getElementById("nprogress")},l.getPositioningCSS=function(){var t=document.body.style,e="WebkitTransform"in t?"Webkit":"MozTransform"in t?"Moz":"msTransform"in t?"ms":"OTransform"in t?"O":"";return e+"Perspective"in t?"translate3d":e+"Transform"in t?"translate":"margin"};var u=function(){function t(){var n=e.shift();n&&n(t)}var e=[];return function(n){e.push(n),1==e.length&&t()}}(),h=function(){function t(t){return t.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,function(t,e){return e.toUpperCase()})}function e(t){var e=document.body.style;if(t in e)return t;for(var n,i=o.length,r=t.charAt(0).toUpperCase()+t.slice(1);i--;)if(n=o[i]+r,n in e)return n;return t}function n(n){return n=t(n),r[n]||(r[n]=e(n))}function i(t,e,i){e=n(e),t.style[e]=i}var o=["Webkit","O","Moz","ms"],r={};return function(t,e){var n,o,r=arguments;if(2==r.length)for(n in e)o=e[n],void 0!==o&&e.hasOwnProperty(n)&&i(t,n,o);else i(t,r[1],r[2])}}();return l}),function(t,e){"use strict";var n;if("object"==typeof exports){try{n=require("moment")}catch(i){}module.exports=e(n)}else"function"==typeof define&&define.amd?define(function(t){var i="moment";try{n=t(i)}catch(o){}return e(n)}):t.Pikaday=e(t.moment)}(this,function(t){"use strict";var e="function"==typeof t,n=!!window.addEventListener,i=window.document,o=window.setTimeout,r=function(t,e,i,o){n?t.addEventListener(e,i,!!o):t.attachEvent("on"+e,i)},s=function(t,e,i,o){n?t.removeEventListener(e,i,!!o):t.detachEvent("on"+e,i)},a=function(t,e,n){var o;i.createEvent?(o=i.createEvent("HTMLEvents"),o.initEvent(e,!0,!1),o=b(o,n),t.dispatchEvent(o)):i.createEventObject&&(o=i.createEventObject(),o=b(o,n),t.fireEvent("on"+e,o))},l=function(t){return t.trim?t.trim():t.replace(/^\s+|\s+$/g,"")},c=function(t,e){return-1!==(" "+t.className+" ").indexOf(" "+e+" ")},u=function(t,e){c(t,e)||(t.className=""===t.className?e:t.className+" "+e)},h=function(t,e){t.className=l((" "+t.className+" ").replace(" "+e+" "," "))},d=function(t){return/Array/.test(Object.prototype.toString.call(t))},f=function(t){return/Date/.test(Object.prototype.toString.call(t))&&!isNaN(t.getTime())},p=function(t){var e=t.getDay();return 0===e||6===e},m=function(t){return t%4===0&&t%100!==0||t%400===0},g=function(t,e){return[31,m(t)?29:28,31,30,31,30,31,31,30,31,30,31][e]},v=function(t){f(t)&&t.setHours(0,0,0,0)},y=function(t,e){return t.getTime()===e.getTime()},b=function(t,e,n){var i,o;for(i in e)o=void 0!==t[i],o&&"object"==typeof e[i]&&null!==e[i]&&void 0===e[i].nodeName?f(e[i])?n&&(t[i]=new Date(e[i].getTime())):d(e[i])?n&&(t[i]=e[i].slice(0)):t[i]=b({},e[i],n):!n&&o||(t[i]=e[i]);return t},_=function(t){return t.month<0&&(t.year-=Math.ceil(Math.abs(t.month)/12),t.month+=12),t.month>11&&(t.year+=Math.floor(Math.abs(t.month)/12),t.month-=12),t},w={field:null,bound:void 0,position:"bottom left",reposition:!0,format:"YYYY-MM-DD",defaultDate:null,setDefaultDate:!1,firstDay:0,minDate:null,maxDate:null,yearRange:10,showWeekNumber:!1,minYear:0,maxYear:9999,minMonth:void 0,maxMonth:void 0,startRange:null,endRange:null,isRTL:!1,yearSuffix:"",showMonthAfterYear:!1,numberOfMonths:1,mainCalendar:"left",container:void 0,i18n:{previousMonth:"Previous Month",nextMonth:"Next Month",months:["January","February","March","April","May","June","July","August","September","October","November","December"],weekdays:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],weekdaysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]},theme:null,onSelect:null,onOpen:null,onClose:null,onDraw:null},x=function(t,e,n){for(e+=t.firstDay;e>=7;)e-=7;return n?t.i18n.weekdaysShort[e]:t.i18n.weekdays[e]},k=function(t){if(t.isEmpty)return'';var e=[];return t.isDisabled&&e.push("is-disabled"),t.isToday&&e.push("is-today"),t.isSelected&&e.push("is-selected"),t.isInRange&&e.push("is-inrange"),t.isStartRange&&e.push("is-startrange"),t.isEndRange&&e.push("is-endrange"),'"},D=function(t,e,n){var i=new Date(n,0,1),o=Math.ceil(((new Date(n,e,t)-i)/864e5+i.getDay()+1)/7);return''+o+""},C=function(t,e){return""+(e?t.reverse():t).join("")+""},T=function(t){return""+t.join("")+""},S=function(t){var e,n=[];for(t.showWeekNumber&&n.push(""),e=0;7>e;e++)n.push(''+x(t,e,!0)+"");return""+(t.isRTL?n.reverse():n).join("")+""},P=function(t,e,n,i,o){var r,s,a,l,c,u=t._o,h=n===u.minYear,f=n===u.maxYear,p='
    ',m=!0,g=!0;for(a=[],r=0;12>r;r++)a.push('");for(l='
    '+u.i18n.months[i]+'
    ",d(u.yearRange)?(r=u.yearRange[0],s=u.yearRange[1]+1):(r=n-u.yearRange,s=1+n+u.yearRange),a=[];s>r&&r<=u.maxYear;r++)r>=u.minYear&&a.push('");return c='
    '+n+u.yearSuffix+'
    ",p+=u.showMonthAfterYear?c+l:l+c,h&&(0===i||u.minMonth>=i)&&(m=!1),f&&(11===i||u.maxMonth<=i)&&(g=!1),0===e&&(p+='"),e===t._o.numberOfMonths-1&&(p+='"),p+="
    "},M=function(t,e){return''+S(t)+T(e)+"
    "},N=function(s){var a=this,l=a.config(s);a._onMouseDown=function(t){if(a._v){t=t||window.event;var e=t.target||t.srcElement;if(e)if(c(e.parentNode,"is-disabled")||(c(e,"pika-button")&&!c(e,"is-empty")?(a.setDate(new Date(e.getAttribute("data-pika-year"),e.getAttribute("data-pika-month"),e.getAttribute("data-pika-day"))),l.bound&&o(function(){a.hide(),l.field&&l.field.blur()},100)):c(e,"pika-prev")?a.prevMonth():c(e,"pika-next")&&a.nextMonth()),c(e,"pika-select"))a._c=!0;else{if(!t.preventDefault)return t.returnValue=!1,!1;t.preventDefault()}}},a._onChange=function(t){t=t||window.event;var e=t.target||t.srcElement;e&&(c(e,"pika-select-month")?a.gotoMonth(e.value):c(e,"pika-select-year")&&a.gotoYear(e.value))},a._onInputChange=function(n){var i;n.firedBy!==a&&(e?(i=t(l.field.value,l.format),i=i&&i.isValid()?i.toDate():null):i=new Date(Date.parse(l.field.value)),f(i)&&a.setDate(i),a._v||a.show())},a._onInputFocus=function(){a.show()},a._onInputClick=function(){a.show()},a._onInputBlur=function(){var t=i.activeElement;do if(c(t,"pika-single"))return;while(t=t.parentNode);a._c||(a._b=o(function(){a.hide()},50)),a._c=!1},a._onClick=function(t){t=t||window.event;var e=t.target||t.srcElement,i=e;if(e){!n&&c(e,"pika-select")&&(e.onchange||(e.setAttribute("onchange","return;"),r(e,"change",a._onChange)));do if(c(i,"pika-single")||i===l.trigger)return;while(i=i.parentNode);a._v&&e!==l.trigger&&i!==l.trigger&&a.hide()}},a.el=i.createElement("div"),a.el.className="pika-single"+(l.isRTL?" is-rtl":"")+(l.theme?" "+l.theme:""),r(a.el,"mousedown",a._onMouseDown,!0),r(a.el,"touchend",a._onMouseDown,!0),r(a.el,"change",a._onChange),l.field&&(l.container?l.container.appendChild(a.el):l.bound?i.body.appendChild(a.el):l.field.parentNode.insertBefore(a.el,l.field.nextSibling),r(l.field,"change",a._onInputChange),l.defaultDate||(e&&l.field.value?l.defaultDate=t(l.field.value,l.format).toDate():l.defaultDate=new Date(Date.parse(l.field.value)),l.setDefaultDate=!0));var u=l.defaultDate;f(u)?l.setDefaultDate?a.setDate(u,!0):a.gotoDate(u):a.gotoDate(new Date),l.bound?(this.hide(),a.el.className+=" is-bound",r(l.trigger,"click",a._onInputClick),r(l.trigger,"focus",a._onInputFocus),r(l.trigger,"blur",a._onInputBlur)):this.show()};return N.prototype={config:function(t){this._o||(this._o=b({},w,!0));var e=b(this._o,t,!0);e.isRTL=!!e.isRTL,e.field=e.field&&e.field.nodeName?e.field:null,e.theme="string"==typeof e.theme&&e.theme?e.theme:null,e.bound=!!(void 0!==e.bound?e.field&&e.bound:e.field),e.trigger=e.trigger&&e.trigger.nodeName?e.trigger:e.field,e.disableWeekends=!!e.disableWeekends,e.disableDayFn="function"==typeof e.disableDayFn?e.disableDayFn:null;var n=parseInt(e.numberOfMonths,10)||1;if(e.numberOfMonths=n>4?4:n,f(e.minDate)||(e.minDate=!1),f(e.maxDate)||(e.maxDate=!1),e.minDate&&e.maxDate&&e.maxDate100&&(e.yearRange=100);return e},toString:function(n){return f(this._d)?e?t(this._d).format(n||this._o.format):this._d.toDateString():""},getMoment:function(){return e?t(this._d):null},setMoment:function(n,i){e&&t.isMoment(n)&&this.setDate(n.toDate(),i)},getDate:function(){return f(this._d)?new Date(this._d.getTime()):null},setDate:function(t,e){if(!t)return this._d=null,this._o.field&&(this._o.field.value="",a(this._o.field,"change",{firedBy:this})),this.draw();if("string"==typeof t&&(t=new Date(Date.parse(t))),f(t)){var n=this._o.minDate,i=this._o.maxDate;f(n)&&n>t?t=n:f(i)&&t>i&&(t=i),this._d=new Date(t.getTime()),v(this._d),this.gotoDate(this._d),this._o.field&&(this._o.field.value=this.toString(),a(this._o.field,"change",{firedBy:this})),e||"function"!=typeof this._o.onSelect||this._o.onSelect.call(this,this.getDate())}},gotoDate:function(t){var e=!0;if(f(t)){if(this.calendars){var n=new Date(this.calendars[0].year,this.calendars[0].month,1),i=new Date(this.calendars[this.calendars.length-1].year,this.calendars[this.calendars.length-1].month,1),o=t.getTime();i.setMonth(i.getMonth()+1),i.setDate(i.getDate()-1),e=o=i&&(this._y=i,!isNaN(s)&&this._m>s&&(this._m=s));for(var l=0;l'+P(this,l,this.calendars[l].year,this.calendars[l].month,this.calendars[0].year)+this.render(this.calendars[l].year,this.calendars[l].month)+"
    ";if(this.el.innerHTML=a,e.bound&&"hidden"!==e.field.type&&o(function(){e.trigger.focus()},1),"function"==typeof this._o.onDraw){var c=this;o(function(){c._o.onDraw.call(c)},0)}}},adjustPosition:function(){var t,e,n,o,r,s,a,l,c,u;if(!this._o.container){if(this.el.style.position="absolute",t=this._o.trigger,e=t,n=this.el.offsetWidth,o=this.el.offsetHeight,r=window.innerWidth||i.documentElement.clientWidth,s=window.innerHeight||i.documentElement.clientHeight,a=window.pageYOffset||i.body.scrollTop||i.documentElement.scrollTop,"function"==typeof t.getBoundingClientRect)u=t.getBoundingClientRect(),l=u.left+window.pageXOffset,c=u.bottom+window.pageYOffset;else for(l=e.offsetLeft,c=e.offsetTop+e.offsetHeight;e=e.offsetParent;)l+=e.offsetLeft,c+=e.offsetTop;(this._o.reposition&&l+n>r||this._o.position.indexOf("right")>-1&&l-n+t.offsetWidth>0)&&(l=l-n+t.offsetWidth),(this._o.reposition&&c+o>s+a||this._o.position.indexOf("top")>-1&&c-o-t.offsetHeight>0)&&(c=c-o-t.offsetHeight),this.el.style.left=l+"px",this.el.style.top=c+"px"}},render:function(t,e){var n=this._o,i=new Date,o=g(t,e),r=new Date(t,e,1).getDay(),s=[],a=[];v(i),n.firstDay>0&&(r-=n.firstDay,0>r&&(r+=7));for(var l=o+r,c=l;c>7;)c-=7;l+=7-c;for(var u=0,h=0;l>u;u++){var d=new Date(t,e,1+(u-r)),m=f(this._d)?y(d,this._d):!1,b=y(d,i),_=r>u||u>=o+r,w=n.startRange&&y(n.startRange,d),x=n.endRange&&y(n.endRange,d),T=n.startRange&&n.endRange&&n.startRangen.maxDate||n.disableWeekends&&p(d)||n.disableDayFn&&n.disableDayFn(d),P={day:1+(u-r),month:e,year:t,isSelected:m,isToday:b,isDisabled:S,isEmpty:_,isStartRange:w,isEndRange:x,isInRange:T};a.push(k(P)),7===++h&&(n.showWeekNumber&&a.unshift(D(u-r,e,t)),s.push(C(a,n.isRTL)),a=[],h=0)}return M(n,s)},isVisible:function(){return this._v},show:function(){this._v||(h(this.el,"is-hidden"),this._v=!0,this.draw(),this._o.bound&&(r(i,"click",this._onClick),this.adjustPosition()),"function"==typeof this._o.onOpen&&this._o.onOpen.call(this))},hide:function(){var t=this._v;t!==!1&&(this._o.bound&&s(i,"click",this._onClick),this.el.style.position="static",this.el.style.left="auto",this.el.style.top="auto",u(this.el,"is-hidden"),this._v=!1,void 0!==t&&"function"==typeof this._o.onClose&&this._o.onClose.call(this))},destroy:function(){this.hide(),s(this.el,"mousedown",this._onMouseDown,!0),s(this.el,"touchend",this._onMouseDown,!0),s(this.el,"change",this._onChange),this._o.field&&(s(this._o.field,"change",this._onInputChange),this._o.bound&&(s(this._o.trigger,"click",this._onInputClick),s(this._o.trigger,"focus",this._onInputFocus),s(this._o.trigger,"blur",this._onInputBlur))),this.el.parentNode&&this.el.parentNode.removeChild(this.el)}},N}),!function(t,e){"object"==typeof module&&"object"==typeof module.exports?module.exports=t.document?e(t,!0):function(t){if(!t.document)throw new Error("jQuery requires a window with a document");return e(t)}:e(t)}("undefined"!=typeof window?window:this,function(t,e){function n(t){var e="length"in t&&t.length,n=K.type(t);return"function"===n||K.isWindow(t)?!1:1===t.nodeType&&e?!0:"array"===n||0===e||"number"==typeof e&&e>0&&e-1 in t}function i(t,e,n){if(K.isFunction(e))return K.grep(t,function(t,i){return!!e.call(t,i,t)!==n});if(e.nodeType)return K.grep(t,function(t){return t===e!==n});if("string"==typeof e){if(at.test(e))return K.filter(e,t,n);e=K.filter(e,t)}return K.grep(t,function(t){return B.call(e,t)>=0!==n})}function o(t,e){for(;(t=t[e])&&1!==t.nodeType;);return t}function r(t){var e=pt[t]={};return K.each(t.match(ft)||[],function(t,n){e[n]=!0}),e}function s(){Z.removeEventListener("DOMContentLoaded",s,!1),t.removeEventListener("load",s,!1),K.ready()}function a(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=K.expando+a.uid++}function l(t,e,n){var i;if(void 0===n&&1===t.nodeType)if(i="data-"+e.replace(_t,"-$1").toLowerCase(),n=t.getAttribute(i),"string"==typeof n){try{n="true"===n?!0:"false"===n?!1:"null"===n?null:+n+""===n?+n:bt.test(n)?K.parseJSON(n):n}catch(o){}yt.set(t,e,n)}else n=void 0;return n}function c(){return!0}function u(){return!1}function h(){try{return Z.activeElement}catch(t){}}function d(t,e){return K.nodeName(t,"table")&&K.nodeName(11!==e.nodeType?e:e.firstChild,"tr")?t.getElementsByTagName("tbody")[0]||t.appendChild(t.ownerDocument.createElement("tbody")):t}function f(t){return t.type=(null!==t.getAttribute("type"))+"/"+t.type,t}function p(t){var e=Yt.exec(t.type);return e?t.type=e[1]:t.removeAttribute("type"),t}function m(t,e){for(var n=0,i=t.length;i>n;n++)vt.set(t[n],"globalEval",!e||vt.get(e[n],"globalEval"))}function g(t,e){var n,i,o,r,s,a,l,c;if(1===e.nodeType){if(vt.hasData(t)&&(r=vt.access(t),s=vt.set(e,r),c=r.events)){delete s.handle,s.events={};for(o in c)for(n=0,i=c[o].length;i>n;n++)K.event.add(e,o,c[o][n])}yt.hasData(t)&&(a=yt.access(t),l=K.extend({},a),yt.set(e,l))}}function v(t,e){var n=t.getElementsByTagName?t.getElementsByTagName(e||"*"):t.querySelectorAll?t.querySelectorAll(e||"*"):[];return void 0===e||e&&K.nodeName(t,e)?K.merge([t],n):n}function y(t,e){var n=e.nodeName.toLowerCase();"input"===n&&Dt.test(t.type)?e.checked=t.checked:("input"===n||"textarea"===n)&&(e.defaultValue=t.defaultValue)}function b(e,n){var i,o=K(n.createElement(e)).appendTo(n.body),r=t.getDefaultComputedStyle&&(i=t.getDefaultComputedStyle(o[0]))?i.display:K.css(o[0],"display");return o.detach(),r}function _(t){var e=Z,n=Ft[t];return n||(n=b(t,e),"none"!==n&&n||(Wt=(Wt||K("